"""Tests for ServerConfig - configuration loading"""

import os
import tempfile
from pathlib import Path

import pytest

from agentkit.server.config import ServerConfig, find_config_path, _resolve_env_vars, _deep_resolve


class TestEnvVarResolution:
    """Test ${VAR:-default} pattern resolution"""

    def test_resolve_simple_var(self):
        os.environ["TEST_AK_KEY"] = "sk-123"
        assert _resolve_env_vars("${TEST_AK_KEY}") == "sk-123"
        del os.environ["TEST_AK_KEY"]

    def test_resolve_var_with_default(self):
        # Var not set -> use default
        assert _resolve_env_vars("${TEST_MISSING_VAR:-fallback}") == "fallback"

    def test_resolve_var_with_default_and_env_set(self):
        os.environ["TEST_AK_KEY"] = "sk-456"
        assert _resolve_env_vars("${TEST_AK_KEY:-fallback}") == "sk-456"
        del os.environ["TEST_AK_KEY"]

    def test_resolve_non_string(self):
        assert _resolve_env_vars(42) == 42
        assert _resolve_env_vars(None) is None

    def test_deep_resolve_dict(self):
        os.environ["TEST_AK_KEY"] = "sk-789"
        data = {"api_key": "${TEST_AK_KEY}", "port": 8001}
        result = _deep_resolve(data)
        assert result["api_key"] == "sk-789"
        assert result["port"] == 8001
        del os.environ["TEST_AK_KEY"]

    def test_deep_resolve_nested(self):
        os.environ["TEST_AK_KEY"] = "sk-nested"
        data = {"llm": {"providers": {"openai": {"api_key": "${TEST_AK_KEY}"}}}}
        result = _deep_resolve(data)
        assert result["llm"]["providers"]["openai"]["api_key"] == "sk-nested"
        del os.environ["TEST_AK_KEY"]


class TestServerConfigFromYaml:
    """Test loading ServerConfig from YAML"""

    def test_load_minimal_config(self):
        yaml_content = """
server:
  host: "127.0.0.1"
  port: 9000
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write(yaml_content)
            f.flush()
            config = ServerConfig.from_yaml(f.name)

        assert config.host == "127.0.0.1"
        assert config.port == 9000
        os.unlink(f.name)

    def test_load_full_config(self):
        yaml_content = """
server:
  host: "0.0.0.0"
  port: 8001
  workers: 4
  api_key: "test-key-123"
  rate_limit: 120

llm:
  default_provider: "openai"
  providers:
    openai:
      api_key: "sk-test"
      base_url: "https://api.openai.com/v1"
      models:
        gpt-4o:
          alias: "default"
        gpt-4o-mini:
          alias: "fast"
    deepseek:
      api_key: "sk-deepseek"
      base_url: "https://api.deepseek.com/v1"
      models:
        deepseek-chat:
          alias: "deepseek"

skills:
  auto_discover: true
  paths:
    - "./skills"

logging:
  level: "DEBUG"
  format: "json"
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write(yaml_content)
            f.flush()
            config = ServerConfig.from_yaml(f.name)

        assert config.host == "0.0.0.0"
        assert config.port == 8001
        assert config.workers == 4
        assert config.api_key == "test-key-123"
        assert config.rate_limit == 120
        assert "openai" in config.llm_config.providers
        assert "deepseek" in config.llm_config.providers
        assert config.llm_config.providers["openai"].api_key == "sk-test"
        assert config.llm_config.model_aliases["default"] == "openai/gpt-4o"
        assert config.llm_config.model_aliases["fast"] == "openai/gpt-4o-mini"
        assert config.skill_paths == ["./skills"]
        assert config.auto_discover_skills is True
        assert config.log_level == "DEBUG"
        assert config.log_format == "json"
        os.unlink(f.name)

    def test_load_config_with_env_vars(self):
        os.environ["TEST_AK_OPENAI_KEY"] = "sk-from-env"
        yaml_content = """
server:
  host: "0.0.0.0"
  port: 8001

llm:
  providers:
    openai:
      api_key: "${TEST_AK_OPENAI_KEY}"
      base_url: "https://api.openai.com/v1"
      models:
        gpt-4o:
          alias: "default"
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write(yaml_content)
            f.flush()
            config = ServerConfig.from_yaml(f.name)

        assert config.llm_config.providers["openai"].api_key == "sk-from-env"
        del os.environ["TEST_AK_OPENAI_KEY"]
        os.unlink(f.name)


class TestServerConfigLoadSkillConfigs:
    """Test loading skill configs from skill paths"""

    def test_load_skills_from_directory(self):
        yaml_content = """
server:
  host: "0.0.0.0"
  port: 8001

skills:
  paths:
    - "./skills"
"""
        with tempfile.TemporaryDirectory() as tmpdir:
            skills_dir = Path(tmpdir) / "skills"
            skills_dir.mkdir()

            # Create a test skill YAML
            skill_yaml = skills_dir / "test_skill.yaml"
            skill_yaml.write_text("""
name: test_skill
agent_type: test
task_mode: llm_generate
supported_tasks:
  - test_task
prompt:
  identity: "Test skill"
""")
            # Update yaml_content with absolute path
            yaml_content_updated = yaml_content.replace("./skills", str(skills_dir))

            with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, dir=tmpdir) as f:
                f.write(yaml_content_updated)
                f.flush()
                config = ServerConfig.from_yaml(f.name)

            configs = config.load_skill_configs()
            assert len(configs) == 1
            assert configs[0].name == "test_skill"
            os.unlink(f.name)

    def test_load_skills_from_single_file(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            skill_yaml = Path(tmpdir) / "my_skill.yaml"
            skill_yaml.write_text("""
name: my_skill
agent_type: test
task_mode: llm_generate
supported_tasks:
  - test_task
prompt:
  identity: "My skill"
""")
            yaml_content = f"""
server:
  host: "0.0.0.0"
  port: 8001

skills:
  paths:
    - "{skill_yaml}"
"""
            with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, dir=tmpdir) as f:
                f.write(yaml_content)
                f.flush()
                config = ServerConfig.from_yaml(f.name)

            configs = config.load_skill_configs()
            assert len(configs) == 1
            assert configs[0].name == "my_skill"
            os.unlink(f.name)

    def test_load_skills_skips_invalid(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            skills_dir = Path(tmpdir) / "skills"
            skills_dir.mkdir()

            # Valid skill
            (skills_dir / "valid.yaml").write_text("""
name: valid_skill
agent_type: test
task_mode: llm_generate
supported_tasks:
  - test
prompt:
  identity: "Valid skill"
""")
            # Invalid skill (missing required fields)
            (skills_dir / "invalid.yaml").write_text("not_a_valid: yaml")

            yaml_content = f"""
server:
  host: "0.0.0.0"
  port: 8001

skills:
  paths:
    - "{skills_dir}"
"""
            with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, dir=tmpdir) as f:
                f.write(yaml_content)
                f.flush()
                config = ServerConfig.from_yaml(f.name)

            configs = config.load_skill_configs()
            assert len(configs) == 1
            assert configs[0].name == "valid_skill"
            os.unlink(f.name)


class TestServerConfigLoadDotenv:
    """Test loading .env file"""

    def test_load_dotenv(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            env_file = Path(tmpdir) / ".env"
            env_file.write_text("MY_TEST_VAR=hello_world\n# comment\nEMPTY_VAR=\n")

            config = ServerConfig()
            config.load_dotenv(str(env_file))

            assert os.environ.get("MY_TEST_VAR") == "hello_world"
            # Cleanup
            del os.environ["MY_TEST_VAR"]

    def test_load_dotenv_no_overwrite(self):
        os.environ["EXISTING_VAR"] = "original"
        with tempfile.TemporaryDirectory() as tmpdir:
            env_file = Path(tmpdir) / ".env"
            env_file.write_text("EXISTING_VAR=should_not_overwrite\n")

            config = ServerConfig()
            config.load_dotenv(str(env_file))

            assert os.environ["EXISTING_VAR"] == "original"
            del os.environ["EXISTING_VAR"]

    def test_load_dotenv_missing_file(self):
        config = ServerConfig()
        config.load_dotenv("/nonexistent/.env")  # Should not raise


class TestFindConfigPath:
    """Test config file discovery"""

    def test_explicit_path_exists(self):
        with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as f:
            f.write(b"test: true")
            f.flush()
            result = find_config_path(f.name)
            assert result == f.name
            os.unlink(f.name)

    def test_explicit_path_not_exists(self):
        result = find_config_path("/nonexistent/agentkit.yaml")
        assert result is None

    def test_find_in_cwd(self):
        original_cwd = os.getcwd()
        with tempfile.TemporaryDirectory() as tmpdir:
            os.chdir(tmpdir)
            config_file = Path(tmpdir) / "agentkit.yaml"
            config_file.write_text("test: true")
            result = find_config_path()
            assert result is not None
            os.chdir(original_cwd)

    def test_no_config_found(self):
        original_cwd = os.getcwd()
        with tempfile.TemporaryDirectory() as tmpdir:
            os.chdir(tmpdir)
            result = find_config_path()
            # May find home dir config, so just check it doesn't crash
            assert result is None or result.endswith("agentkit.yaml")
            os.chdir(original_cwd)


class TestConfigHotReload:
    """Test config file watching and hot-reload"""

    def test_config_change_triggers_callback(self):
        """Config change triggers on_change callback with new config"""
        import time

        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write("server:\n  host: '0.0.0.0'\n  port: 8001\n")
            f.flush()
            config_path = f.name

        config = ServerConfig.from_yaml(config_path)
        assert config.port == 8001

        callback_called = []
        config.on_change = lambda cfg: callback_called.append(cfg.port)

        # Modify the config file
        time.sleep(0.1)  # Ensure mtime changes
        with open(config_path, "w") as f:
            f.write("server:\n  host: '0.0.0.0'\n  port: 9000\n")

        # Manually trigger reload (simulating what the watcher does)
        config._try_reload_config(config_path)

        assert config.port == 9000
        assert callback_called == [9000]

        os.unlink(config_path)

    def test_invalid_config_does_not_overwrite(self):
        """Invalid config file doesn't overwrite current config"""
        import time

        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write("server:\n  host: '0.0.0.0'\n  port: 8001\n")
            f.flush()
            config_path = f.name

        config = ServerConfig.from_yaml(config_path)
        assert config.port == 8001

        # Write invalid YAML
        with open(config_path, "w") as f:
            f.write("{{invalid yaml:::\n")

        # Should not crash and should keep current config
        config._try_reload_config(config_path)
        assert config.port == 8001  # Unchanged

        os.unlink(config_path)

    def test_stop_watching(self):
        """stop_watching cancels the watcher task"""
        import asyncio

        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write("server:\n  host: '0.0.0.0'\n  port: 8001\n")
            f.flush()
            config_path = f.name

        config = ServerConfig.from_yaml(config_path)

        async def _test():
            # Start watching (will use polling fallback since watchfiles may not be installed)
            config.watch_config()
            assert config._watcher_task is not None

            # Give the watcher a moment to start
            await asyncio.sleep(0.05)

            # Stop watching
            config.stop_watching()
            # The task should be cancelled
            assert config._watcher_task is None or config._watcher_task.done()

        asyncio.run(_test())
        os.unlink(config_path)

    def test_watch_config_without_path_warns(self):
        """watch_config without a path and no stored path logs warning"""
        config = ServerConfig()
        # Should not raise, just log a warning
        config.watch_config()
        assert config._watcher_task is None

    def test_from_yaml_stores_config_path(self):
        """from_yaml stores the config path for later watching"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write("server:\n  host: '0.0.0.0'\n  port: 8001\n")
            f.flush()
            config_path = f.name

        config = ServerConfig.from_yaml(config_path)
        assert config._config_path == config_path
        assert config._last_mtime > 0

        os.unlink(config_path)

    def test_reload_preserves_config_path(self):
        """After reload, _config_path is still set"""
        import time

        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write("server:\n  host: '0.0.0.0'\n  port: 8001\n")
            f.flush()
            config_path = f.name

        config = ServerConfig.from_yaml(config_path)

        time.sleep(0.1)
        with open(config_path, "w") as f:
            f.write("server:\n  host: '0.0.0.0'\n  port: 9000\n")

        config._try_reload_config(config_path)
        assert config._config_path == config_path
        assert config.port == 9000

        os.unlink(config_path)
