"""Unit tests for configuration management system."""

import os
import tempfile
from pathlib import Path

import pytest
import yaml

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


class TestCostStructure:
    """Test CostStructure configuration model."""

    def test_default_values(self) -> None:
        """Test default cost structure values."""
        cost = CostStructure()
        assert cost.llm_input_per_1m == 2.5
        assert cost.llm_output_per_1m == 10.0
        assert cost.market_data_per_call == 0.01
        assert cost.trade_fee_rate == 0.001

    def test_custom_values(self) -> None:
        """Test custom cost structure values."""
        cost = CostStructure(
            llm_input_per_1m=5.0,
            llm_output_per_1m=20.0,
            market_data_per_call=0.02,
            trade_fee_rate=0.002,
        )
        assert cost.llm_input_per_1m == 5.0
        assert cost.llm_output_per_1m == 20.0
        assert cost.market_data_per_call == 0.02
        assert cost.trade_fee_rate == 0.002

    def test_validation_positive_values(self) -> None:
        """Test validation of positive values."""
        with pytest.raises(ValueError):
            CostStructure(llm_input_per_1m=-1.0)

        with pytest.raises(ValueError):
            CostStructure(llm_output_per_1m=0)

        with pytest.raises(ValueError):
            CostStructure(market_data_per_call=-0.01)

        with pytest.raises(ValueError):
            CostStructure(trade_fee_rate=-0.001)

        with pytest.raises(ValueError):
            CostStructure(trade_fee_rate=1.5)


class TestSurvivalThresholds:
    """Test SurvivalThresholds configuration model."""

    def test_default_values(self) -> None:
        """Test default survival threshold values."""
        thresholds = SurvivalThresholds()
        assert thresholds.thriving_multiplier == 3.0
        assert thresholds.stable_multiplier == 1.5
        assert thresholds.struggling_multiplier == 0.8
        assert thresholds.bankrupt_multiplier == 0.1

    def test_threshold_order(self) -> None:
        """Test that thresholds are in correct order."""
        thresholds = SurvivalThresholds()
        assert thresholds.thriving_multiplier > thresholds.stable_multiplier
        assert thresholds.stable_multiplier > thresholds.struggling_multiplier
        assert thresholds.struggling_multiplier > thresholds.bankrupt_multiplier

    def test_validation_greater_than_one(self) -> None:
        """Test thriving multiplier must be > 1."""
        with pytest.raises(ValueError):
            SurvivalThresholds(thriving_multiplier=0.5)

        with pytest.raises(ValueError):
            SurvivalThresholds(thriving_multiplier=1.0)


class TestLLMConfig:
    """Test LLMConfig configuration model."""

    def test_default_values(self) -> None:
        """Test default LLM configuration values."""
        config = LLMConfig()
        assert config.model == "gpt-4o"
        assert config.temperature == 0.7
        assert config.timeout == 30
        assert config.api_key is None
        assert config.base_url is None

    def test_custom_values(self) -> None:
        """Test custom LLM configuration values."""
        config = LLMConfig(
            model="claude-3-5-sonnet",
            temperature=0.5,
            timeout=60,
            api_key="test-key",
            base_url="https://api.example.com",
        )
        assert config.model == "claude-3-5-sonnet"
        assert config.temperature == 0.5
        assert config.timeout == 60
        assert config.api_key == "test-key"
        assert config.base_url == "https://api.example.com"

    def test_temperature_validation(self) -> None:
        """Test temperature must be between 0 and 2."""
        with pytest.raises(ValueError):
            LLMConfig(temperature=-0.1)

        with pytest.raises(ValueError):
            LLMConfig(temperature=2.1)


class TestOpenClawConfig:
    """Test OpenClawConfig main configuration model."""

    def test_default_initialization(self) -> None:
        """Test default configuration initialization."""
        config = OpenClawConfig()
        assert config.initial_capital["trader"] == 10000.0
        assert config.initial_capital["analyst"] == 5000.0
        assert config.cost_structure.llm_input_per_1m == 2.5
        assert config.simulation_days == 30
        assert config.log_level == "INFO"

    def test_initial_capital_validation(self) -> None:
        """Test initial capital must be positive."""
        with pytest.raises(ValueError):
            OpenClawConfig(initial_capital={"trader": -1000.0})

        with pytest.raises(ValueError):
            OpenClawConfig(initial_capital={"analyst": 0.0})

    def test_nested_models(self) -> None:
        """Test nested configuration models."""
        config = OpenClawConfig(
            cost_structure=CostStructure(llm_input_per_1m=5.0),
            survival_thresholds=SurvivalThresholds(thriving_multiplier=4.0),
        )
        assert config.cost_structure.llm_input_per_1m == 5.0
        assert config.survival_thresholds.thriving_multiplier == 4.0


class TestConfigLoading:
    """Test configuration loading from files."""

    def test_load_from_yaml(self, tmp_path: Path) -> None:
        """Test loading configuration from YAML file."""
        config_file = tmp_path / "test_config.yaml"
        config_data = {
            "initial_capital": {"trader": 20000.0, "analyst": 10000.0},
            "simulation_days": 60,
            "log_level": "DEBUG",
        }
        config_file.write_text(yaml.dump(config_data))

        config = ConfigLoader.load(config_file)
        assert config.initial_capital["trader"] == 20000.0
        assert config.initial_capital["analyst"] == 10000.0
        assert config.simulation_days == 60
        assert config.log_level == "DEBUG"

    def test_load_from_json(self, tmp_path: Path) -> None:
        """Test loading configuration from JSON file."""
        config_file = tmp_path / "test_config.json"
        config_data = {
            "initial_capital": {"trader": 15000.0},
            "cost_structure": {"llm_input_per_1m": 3.0},
        }
        import json

        config_file.write_text(json.dumps(config_data))

        config = ConfigLoader.load(config_file)
        assert config.initial_capital["trader"] == 15000.0
        assert config.cost_structure.llm_input_per_1m == 3.0

    def test_load_nonexistent_file(self) -> None:
        """Test loading from non-existent file raises FileNotFoundError."""
        with pytest.raises(FileNotFoundError):
            ConfigLoader.load("/nonexistent/config.yaml")

    def test_create_default_config(self, tmp_path: Path) -> None:
        """Test creating default configuration file."""
        output_path = tmp_path / "default_config.yaml"
        path = ConfigLoader.create_default_config(output_path)
        assert path.exists()

        # Verify it can be loaded
        config = ConfigLoader.load(path)
        assert config.initial_capital["trader"] == 10000.0


class TestEnvironmentVariables:
    """Test environment variable overrides."""

    def test_env_prefix_filtering(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Test that environment variables with correct prefix are used."""
        # This tests the SettingsConfigDict env_prefix behavior
        monkeypatch.setenv("OPENCLAW_SIMULATION_DAYS", "100")
        monkeypatch.setenv("OPENCLAW_LOG_LEVEL", "ERROR")

        # Reload to pick up environment variables
        config = reload_config()
        assert config.simulation_days == 100
        assert config.log_level == "ERROR"

    def test_env_nested_values(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Test environment variables for nested config values."""
        # Nested values should be accessible via double underscore
        monkeypatch.setenv("OPENCLAW_COST_STRUCTURE__LLM_INPUT_PER_1M", "5.5")

        config = reload_config()
        assert config.cost_structure.llm_input_per_1m == 5.5


class TestGlobalConfig:
    """Test global configuration instance."""

    def test_get_config_singleton(self) -> None:
        """Test that get_config returns the same instance."""
        config1 = get_config()
        config2 = get_config()
        assert config1 is config2

    def test_reload_config_updates_global(self) -> None:
        """Test that reload_config updates the global instance."""
        # First clear any existing config
        from openclaw.core.config import set_config
        set_config(None)

        config1 = get_config()
        config2 = reload_config()
        # reload_config creates a new instance and updates the global
        # config1 is the old instance, config2 is the new global instance
        assert config1 is not config2  # Different instances
        assert config2 is get_config()  # But config2 is now the global


class TestConfigValidation:
    """Test configuration validation edge cases."""

    def test_empty_initial_capital(self) -> None:
        """Test empty initial capital dict."""
        config = OpenClawConfig(initial_capital={})
        # Should use empty dict, not defaults
        assert config.initial_capital == {}

    def test_partial_config_file(self, tmp_path: Path) -> None:
        """Test loading partial configuration from file."""
        config_file = tmp_path / "partial.yaml"
        config_file.write_text("simulation_days: 45\n")

        config = ConfigLoader.load(config_file)
        assert config.simulation_days == 45
        # Other values should use defaults
        assert config.initial_capital["trader"] == 10000.0

    def test_invalid_yaml_raises_error(self, tmp_path: Path) -> None:
        """Test that invalid YAML raises ValueError."""
        config_file = tmp_path / "invalid.yaml"
        config_file.write_text("invalid: yaml: content: [")

        # Should raise ValueError for invalid YAML
        with pytest.raises(ValueError, match="Invalid YAML"):
            ConfigLoader.load(config_file)
