"""Tests for configuration management API endpoints.

Tests the REST API endpoints for configuration management:
- GET /api/config - Returns current configuration
- POST /api/config - Saves and validates configuration
- GET /api/config/schema - Returns JSON schema for form generation
"""

import json
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import patch

import pytest
import yaml
from fastapi.testclient import TestClient

from openclaw.dashboard.app import create_app
from openclaw.core.config import (
    ConfigLoader,
    CostStructure,
    LLMConfig,
    OpenClawConfig,
    SurvivalThresholds,
    get_config,
    reload_config,
    set_config,
)


@pytest.fixture
def app():
    """Create a fresh FastAPI application for testing."""
    return create_app()


@pytest.fixture
def client(app):
    """Create a TestClient for the FastAPI application."""
    return TestClient(app)


@pytest.fixture(autouse=True)
def reset_config():
    """Reset global config before and after each test."""
    # Clear any existing config
    set_config(None)
    yield
    # Clean up after test
    set_config(None)


@pytest.fixture
def temp_config_file(tmp_path: Path) -> Path:
    """Create a temporary config file for testing."""
    config_file = tmp_path / "test_config.yaml"
    config_data = {
        "initial_capital": {"trader": 15000.0, "analyst": 7500.0},
        "simulation_days": 60,
        "log_level": "DEBUG",
        "cost_structure": {
            "llm_input_per_1m": 3.0,
            "llm_output_per_1m": 12.0,
            "market_data_per_call": 0.02,
            "trade_fee_rate": 0.002,
        },
        "survival_thresholds": {
            "thriving_multiplier": 2.5,
            "stable_multiplier": 1.3,
            "struggling_multiplier": 0.7,
            "bankrupt_multiplier": 0.1,
        },
    }
    config_file.write_text(yaml.dump(config_data))
    return config_file


@pytest.fixture
def valid_config_payload() -> dict[str, Any]:
    """Return a valid configuration payload for API testing."""
    return {
        "initial_capital": {"trader": 20000.0, "analyst": 10000.0, "risk_manager": 8000.0},
        "simulation_days": 90,
        "log_level": "INFO",
        "data_dir": "./data",
        "cost_structure": {
            "llm_input_per_1m": 2.5,
            "llm_output_per_1m": 10.0,
            "market_data_per_call": 0.01,
            "trade_fee_rate": 0.001,
        },
        "survival_thresholds": {
            "thriving_multiplier": 3.0,
            "stable_multiplier": 1.5,
            "struggling_multiplier": 0.8,
            "bankrupt_multiplier": 0.1,
        },
        "llm_providers": {
            "openai": {
                "model": "gpt-4o",
                "temperature": 0.7,
                "timeout": 30,
                "api_key": None,
                "base_url": None,
                "max_tokens": None,
            }
        },
    }


class TestGetConfig:
    """Tests for GET /api/config endpoint."""

    def test_get_config_returns_valid_config(self, client: TestClient) -> None:
        """Test that GET /api/config returns a valid configuration object."""
        response = client.get("/api/config")

        assert response.status_code == 200
        data = response.json()

        # Check required fields are present
        assert "initial_capital" in data
        assert "simulation_days" in data
        assert "log_level" in data
        assert "cost_structure" in data
        assert "survival_thresholds" in data

    def test_get_config_default_values(self, client: TestClient) -> None:
        """Test that GET /api/config returns default values when no config file exists."""
        response = client.get("/api/config")

        assert response.status_code == 200
        data = response.json()

        # Verify default values
        assert data["initial_capital"]["trader"] == 10000.0
        assert data["initial_capital"]["analyst"] == 5000.0
        assert data["simulation_days"] == 30
        assert data["log_level"] == "INFO"
        assert data["cost_structure"]["llm_input_per_1m"] == 2.5
        assert data["cost_structure"]["trade_fee_rate"] == 0.001

    def test_get_config_loaded_from_file(
        self, client: TestClient, temp_config_file: Path
    ) -> None:
        """Test that GET /api/config returns config loaded from file."""
        # Load the temp config file
        with patch.object(ConfigLoader, "_resolve_config_path", return_value=temp_config_file):
            set_config(None)  # Clear cache
            response = client.get("/api/config")

        assert response.status_code == 200
        data = response.json()

        # Verify values from file
        assert data["initial_capital"]["trader"] == 15000.0
        assert data["simulation_days"] == 60
        assert data["log_level"] == "DEBUG"


class TestPostConfig:
    """Tests for POST /api/config endpoint."""

    def test_post_valid_config_saves_successfully(
        self, client: TestClient, valid_config_payload: dict[str, Any]
    ) -> None:
        """Test that POST /api/config saves valid configuration."""
        response = client.post("/api/config", json=valid_config_payload)

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "success"
        assert "message" in data

    def test_post_config_updates_values(
        self, client: TestClient, valid_config_payload: dict[str, Any]
    ) -> None:
        """Test that POST /api/config updates configuration values."""
        # Post new config
        response = client.post("/api/config", json=valid_config_payload)
        assert response.status_code == 200

        # Verify values were updated by getting config
        response = client.get("/api/config")
        data = response.json()

        assert data["simulation_days"] == 90
        assert data["initial_capital"]["trader"] == 20000.0
        assert data["log_level"] == "INFO"

    def test_post_config_partial_update(
        self, client: TestClient
    ) -> None:
        """Test that POST /api/config handles partial updates."""
        partial_config = {"simulation_days": 45, "log_level": "WARNING"}

        response = client.post("/api/config", json=partial_config)

        assert response.status_code == 200

        # Verify only specified fields were updated
        response = client.get("/api/config")
        data = response.json()

        assert data["simulation_days"] == 45
        assert data["log_level"] == "WARNING"
        # Other fields should retain default values
        assert data["initial_capital"]["trader"] == 10000.0

    def test_post_invalid_config_rejected(
        self, client: TestClient
    ) -> None:
        """Test that POST /api/config rejects invalid configuration values."""
        invalid_config = {
            "simulation_days": -5,  # Invalid: must be positive
        }

        response = client.post("/api/config", json=invalid_config)

        # API returns 200 with error status instead of 422
        assert response.status_code == 200
        data = response.json()
        assert data.get("status") == "error"
        assert "simulation_days" in data.get("message", "").lower() or "validation" in data.get("message", "").lower()

    def test_post_invalid_log_level_rejected(self, client: TestClient) -> None:
        """Test that invalid log level is rejected."""
        invalid_config = {"log_level": "INVALID"}

        response = client.post("/api/config", json=invalid_config)

        assert response.status_code == 200
        data = response.json()
        assert data.get("status") == "error"

    def test_post_invalid_cost_structure_rejected(self, client: TestClient) -> None:
        """Test that invalid cost structure values are rejected."""
        invalid_config = {
            "cost_structure": {
                "llm_input_per_1m": -1.0,  # Invalid: must be positive
            }
        }

        response = client.post("/api/config", json=invalid_config)

        assert response.status_code == 200
        data = response.json()
        assert data.get("status") == "error"

    def test_post_invalid_survival_thresholds_rejected(self, client: TestClient) -> None:
        """Test that invalid survival threshold values are rejected."""
        invalid_config = {
            "survival_thresholds": {
                "thriving_multiplier": 0.5,  # Invalid: must be > 1
            }
        }

        response = client.post("/api/config", json=invalid_config)

        assert response.status_code == 200
        data = response.json()
        assert data.get("status") == "error"

    def test_post_invalid_trade_fee_rate_rejected(self, client: TestClient) -> None:
        """Test that trade fee rate > 1 is rejected."""
        invalid_config = {
            "cost_structure": {
                "trade_fee_rate": 1.5,  # Invalid: must be <= 1
            }
        }

        response = client.post("/api/config", json=invalid_config)

        assert response.status_code == 200
        data = response.json()
        assert data.get("status") == "error"


class TestGetConfigSchema:
    """Tests for GET /api/config/schema endpoint."""

    def test_get_config_schema_returns_schema(self, client: TestClient) -> None:
        """Test that GET /api/config/schema returns JSON schema."""
        response = client.get("/api/config/schema")

        assert response.status_code == 200
        data = response.json()

        # Check schema structure
        assert "title" in data or "$defs" in data or "properties" in data

    def test_get_config_schema_contains_expected_fields(self, client: TestClient) -> None:
        """Test that schema contains expected configuration fields."""
        response = client.get("/api/config/schema")

        assert response.status_code == 200
        data = response.json()

        # Schema should contain properties or definitions
        schema_str = json.dumps(data).lower()
        assert "initial_capital" in schema_str or "initialcapital" in schema_str
        assert "simulation_days" in schema_str or "simulationdays" in schema_str
        assert "log_level" in schema_str or "loglevel" in schema_str


class TestConfigRoundtrip:
    """Tests for config save/load roundtrip."""

    def test_config_roundtrip(
        self, client: TestClient, valid_config_payload: dict[str, Any]
    ) -> None:
        """Test that config can be saved and then loaded with same values."""
        # Save config
        response = client.post("/api/config", json=valid_config_payload)
        assert response.status_code == 200

        # Load config and verify values match
        response = client.get("/api/config")
        data = response.json()

        assert data["simulation_days"] == valid_config_payload["simulation_days"]
        assert data["log_level"] == valid_config_payload["log_level"]
        assert data["initial_capital"]["trader"] == valid_config_payload["initial_capital"]["trader"]

    def test_multiple_updates_preserve_values(
        self, client: TestClient
    ) -> None:
        """Test that multiple config updates work correctly."""
        # First update
        config1 = {"simulation_days": 30, "log_level": "INFO"}
        response = client.post("/api/config", json=config1)
        assert response.status_code == 200

        # Second update
        config2 = {"simulation_days": 60}
        response = client.post("/api/config", json=config2)
        assert response.status_code == 200

        # Verify final state
        response = client.get("/api/config")
        data = response.json()

        assert data["simulation_days"] == 60
        assert data["log_level"] == "INFO"  # Should retain from first update


class TestConfigValidation:
    """Tests for configuration validation in API."""

    def test_negative_initial_capital_rejected(self, client: TestClient) -> None:
        """Test that negative initial capital values are rejected."""
        invalid_config = {
            "initial_capital": {"trader": -1000.0}
        }

        response = client.post("/api/config", json=invalid_config)

        assert response.status_code == 200
        data = response.json()
        assert data.get("status") == "error"

    def test_zero_simulation_days_rejected(self, client: TestClient) -> None:
        """Test that zero simulation days is rejected."""
        invalid_config = {"simulation_days": 0}

        response = client.post("/api/config", json=invalid_config)

        assert response.status_code == 200
        data = response.json()
        assert data.get("status") == "error"

    def test_invalid_temperature_rejected(self, client: TestClient) -> None:
        """Test that invalid temperature values are rejected."""
        invalid_config = {
            "llm_providers": {
                "openai": {
                    "model": "gpt-4o",
                    "temperature": 3.0,  # Invalid: must be <= 2
                }
            }
        }

        response = client.post("/api/config", json=invalid_config)

        assert response.status_code == 200
        data = response.json()
        assert data.get("status") == "error"

    def test_nested_validation_error_detail(self, client: TestClient) -> None:
        """Test that validation errors include detail about nested fields."""
        invalid_config = {
            "cost_structure": {
                "llm_input_per_1m": -5.0,
            }
        }

        response = client.post("/api/config", json=invalid_config)

        assert response.status_code == 200
        data = response.json()
        # Should have error status with message
        assert data.get("status") == "error"
        assert "message" in data


class TestErrorHandling:
    """Tests for API error handling."""

    def test_malformed_json_rejected(self, client: TestClient) -> None:
        """Test that malformed JSON is handled gracefully."""
        response = client.post(
            "/api/config",
            data="{invalid json",
            headers={"Content-Type": "application/json"},
        )

        assert response.status_code == 422

    def test_empty_body_rejected(self, client: TestClient) -> None:
        """Test that empty request body is handled."""
        response = client.post("/api/config", json={})

        # Empty config should be valid (uses defaults)
        assert response.status_code == 200

    def test_invalid_content_type_handled(self, client: TestClient) -> None:
        """Test that invalid content type is handled."""
        response = client.post(
            "/api/config",
            data="not json",
            headers={"Content-Type": "text/plain"},
        )

        # FastAPI should return 415 Unsupported Media Type or 422
        assert response.status_code in [415, 422]
