"""
测试配置合并机制 —— 老用户升级时自动补全新增字段
"""
import os
import sys
import tempfile
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest
import yaml
import platformdirs as pd


class TestDeepMerge:
    """_deep_merge 静态方法单元测试"""

    def test_add_missing_top_level_key(self):
        from aacode.config import Settings
        base = {"a": 1, "b": 2}
        override = {"a": 10}
        result = Settings._deep_merge(base, override)
        assert result == {"a": 10, "b": 2}

    def test_preserve_override_values(self):
        from aacode.config import Settings
        base = {"a": 1, "b": 2}
        override = {"a": 10, "b": 20}
        result = Settings._deep_merge(base, override)
        assert result == {"a": 10, "b": 20}

    def test_nested_dict_merge(self):
        from aacode.config import Settings
        base = {"tools": {"enable_shell": True, "enable_search": False, "new_field": "default"}}
        override = {"tools": {"enable_shell": False}}
        result = Settings._deep_merge(base, override)
        assert result["tools"]["enable_shell"] is False  # override wins
        assert result["tools"]["enable_search"] is False  # base preserved (not in override)
        assert result["tools"]["new_field"] == "default"  # base added

    def test_override_has_extra_keys(self):
        from aacode.config import Settings
        base = {"a": 1}
        override = {"a": 10, "extra": "value"}
        result = Settings._deep_merge(base, override)
        assert result == {"a": 10, "extra": "value"}

    def test_lists_preserved_in_override(self):
        """列表不会被深度合并，override 的值完全保留"""
        from aacode.config import Settings
        base = {"mcp": {"std_servers": [{"name": "default_server"}]}}
        override = {"mcp": {"std_servers": [{"name": "my_custom_server"}]}}
        result = Settings._deep_merge(base, override)
        assert result["mcp"]["std_servers"] == [{"name": "my_custom_server"}]

    def test_empty_override(self):
        from aacode.config import Settings
        base = {"a": 1, "b": {"c": 2}}
        override = {}
        result = Settings._deep_merge(base, override)
        assert result == {"a": 1, "b": {"c": 2}}

    def test_deeply_nested(self):
        from aacode.config import Settings
        base = {"a": {"b": {"c": 1, "d": 2}}}
        override = {"a": {"b": {"c": 10}}}
        result = Settings._deep_merge(base, override)
        assert result == {"a": {"b": {"c": 10, "d": 2}}}


class TestMergeWithDefaults:
    """_merge_with_defaults 方法测试"""

    def test_missing_sections_added(self):
        """用户配置只有 model 段时，其他段被补充"""
        from aacode.config import Settings
        user = {"model": {"name": "my-model", "max_tokens": 8000}}
        s = Settings.__new__(Settings)  # 绕过 __init__
        merged = s._merge_with_defaults(user)
        assert "tools" in merged
        assert "safety" in merged
        assert "context" in merged
        assert "agent" in merged
        assert "mcp" in merged
        assert "timeouts" in merged
        assert "limits" in merged
        assert "skills" in merged
        assert "multimodal" in merged
        assert "output" in merged
        assert "language" in merged
        # 用户自定义值保留
        assert merged["model"]["name"] == "my-model"
        assert merged["model"]["max_tokens"] == 8000

    def test_user_values_not_overwritten(self):
        """用户已有值不被默认值覆盖"""
        from aacode.config import Settings
        user = {
            "model": {"max_tokens": 32000},
            "agent": {"max_react_iterations": 50},
            "tools": {"enable_sandbox": False},
        }
        s = Settings.__new__(Settings)
        merged = s._merge_with_defaults(user)
        assert merged["model"]["max_tokens"] == 32000
        assert merged["agent"]["max_react_iterations"] == 50
        assert merged["tools"]["enable_sandbox"] is False

    def test_partial_section_filled(self):
        """用户配置中某个段只有部分字段，缺失字段被补全"""
        from aacode.config import Settings
        user = {"tools": {"enable_shell": False}}
        s = Settings.__new__(Settings)
        merged = s._merge_with_defaults(user)
        assert merged["tools"]["enable_shell"] is False  # 用户值
        assert "enable_search" in merged["tools"]  # 补全
        assert "enable_web_search" in merged["tools"]  # 补全


class TestConfigMergeOnLoad:
    """加载时自动合并并写回磁盘"""

    def test_old_config_gets_merged_and_saved(self, monkeypatch):
        """老配置（缺字段）加载后被合并并写回磁盘"""
        from aacode.config import Settings
        old_config = {
            "model": {"name": "old-model", "max_tokens": 16000},
            "tools": {"enable_shell": True, "enable_search": True},
        }
        with tempfile.TemporaryDirectory() as td:
            monkeypatch.setattr(pd, "user_config_dir", lambda *a, **kw: str(td))
            cfg_path = Path(td) / "aacode_config.yaml"
            cfg_path.parent.mkdir(parents=True, exist_ok=True)
            cfg_path.write_text(yaml.dump(old_config), encoding="utf-8")

            s = Settings()
            # 配置被合并 - 老字段还在
            assert s.model.name == "old-model"
            assert s.model.max_tokens == 16000
            # 新字段被补全（来自默认值）
            assert s.tools.enable_web_search is True  # 包内默认值
            assert s.agent.max_react_iterations == 300  # 包内默认值

            # 验证合并后的内容写回到了磁盘
            saved = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
            assert "agent" in saved
            assert saved["agent"]["max_react_iterations"] == 300
            assert saved["model"]["name"] == "old-model"
            assert "context" in saved
            assert "safety" in saved

    def test_user_values_preserved_after_merge(self, monkeypatch):
        """用户自定义值在合并后不被覆盖"""
        from aacode.config import Settings
        user_config = {
            "model": {"name": "custom-model", "temperature": 0.5},
            "agent": {"max_react_iterations": 42},
            "tools": {"enable_web_search": False, "search_api_key": "my-secret-key"},
        }
        with tempfile.TemporaryDirectory() as td:
            monkeypatch.setattr(pd, "user_config_dir", lambda *a, **kw: str(td))
            cfg_path = Path(td) / "aacode_config.yaml"
            cfg_path.parent.mkdir(parents=True, exist_ok=True)
            cfg_path.write_text(yaml.dump(user_config), encoding="utf-8")

            s = Settings()
            assert s.model.name == "custom-model"
            assert s.model.temperature == 0.5
            assert s.agent.max_react_iterations == 42
            assert s.tools.enable_web_search is False
            assert s.tools.search_api_key == "my-secret-key"

            saved = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
            assert saved["model"]["temperature"] == 0.5
            assert saved["agent"]["max_react_iterations"] == 42
            assert saved["tools"]["search_api_key"] == "my-secret-key"

    def test_package_source_not_merged(self, monkeypatch):
        """package 来源的配置不触发合并（本身就是默认值）"""
        from aacode.config import Settings

        with tempfile.TemporaryDirectory() as home_td:
            with tempfile.TemporaryDirectory() as pd_td:
                monkeypatch.setattr(pd, "user_config_dir", lambda *a, **kw: str(pd_td))
                monkeypatch.setattr(Path, "home", lambda: Path(home_td))
                # 确保 home 和 platformdirs 下都没有配置，这样会回退到 package
                s = Settings()
                assert s.config_source == "package"
                assert s.model.max_tokens == 96000

    def test_explicit_config_gets_merged(self, monkeypatch):
        """显式指定的配置文件也会触发合并"""
        from aacode.config import Settings
        minimal_config = {
            "model": {"name": "explicit-model"},
        }
        with tempfile.TemporaryDirectory() as td:
            cfg_path = Path(td) / "my_config.yaml"
            cfg_path.write_text(yaml.dump(minimal_config), encoding="utf-8")

            s = Settings(config_file=str(cfg_path))
            assert s.config_source == "explicit"
            assert s.model.name == "explicit-model"
            # 缺失字段被补全
            assert s.tools.enable_web_search is True
            assert s.agent.max_react_iterations == 300

            # 验证写回
            saved = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
            assert "tools" in saved
            assert "agent" in saved

    def test_no_merge_when_no_changes(self, monkeypatch):
        """配置已是最新时不会重复写入"""
        from aacode.config import Settings

        with tempfile.TemporaryDirectory() as td:
            monkeypatch.setattr(pd, "user_config_dir", lambda *a, **kw: str(td))

            # 先创建一份完整的默认配置
            from pathlib import Path as P
            pkg_config_path = P(__file__).parent.parent / "aacode" / "aacode_config.yaml"
            full_default = yaml.safe_load(pkg_config_path.read_text(encoding="utf-8"))

            cfg_path = Path(td) / "aacode_config.yaml"
            cfg_path.parent.mkdir(parents=True, exist_ok=True)
            cfg_path.write_text(yaml.dump(full_default), encoding="utf-8")

            import time
            mtime_before = cfg_path.stat().st_mtime
            # 稍微等一下确保时间戳不同
            time.sleep(0.01)

            s = Settings()
            mtime_after = cfg_path.stat().st_mtime
            # 文件不应被修改（config 一致，无合并）
            assert mtime_before == mtime_after

    def test_mcp_lists_preserved(self, monkeypatch):
        """MCP 的 std_servers/sse_servers 列表用户自定义值被保留"""
        from aacode.config import Settings
        user_config = {
            "model": {"name": "test-model"},
            "mcp": {
                "enabled": True,
                "std_servers": [{"name": "my_server", "command": "echo", "args": ["hello"], "enabled": True}],
            },
        }
        with tempfile.TemporaryDirectory() as td:
            monkeypatch.setattr(pd, "user_config_dir", lambda *a, **kw: str(td))
            cfg_path = Path(td) / "aacode_config.yaml"
            cfg_path.parent.mkdir(parents=True, exist_ok=True)
            cfg_path.write_text(yaml.dump(user_config), encoding="utf-8")

            s = Settings()
            assert len(s.mcp.std_servers) == 1
            assert s.mcp.std_servers[0]["name"] == "my_server"
            assert s.mcp.std_servers[0]["command"] == "echo"

            saved = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
            assert len(saved["mcp"]["std_servers"]) == 1
            assert saved["mcp"]["std_servers"][0]["name"] == "my_server"

    def test_multimodal_models_preserved(self, monkeypatch):
        """多模态 models 字典用户已配模型保留，缺失的模型被补全"""
        from aacode.config import Settings
        user_config = {
            "model": {"name": "test-model"},
            "multimodal": {
                "enabled": True,
                "default_model": "my_custom_model",
                "models": {
                    "my_custom_model": {
                        "name": "my-vision-v1",
                        "provider": "custom",
                        "gateway": "openai",
                        "base_url": "https://my.api.com/v1",
                        "api_key": "",
                        "vision": True,
                        "video": False,
                    }
                },
            },
        }
        with tempfile.TemporaryDirectory() as td:
            monkeypatch.setattr(pd, "user_config_dir", lambda *a, **kw: str(td))
            cfg_path = Path(td) / "aacode_config.yaml"
            cfg_path.parent.mkdir(parents=True, exist_ok=True)
            cfg_path.write_text(yaml.dump(user_config), encoding="utf-8")

            s = Settings()
            assert s.multimodal.default_model == "my_custom_model"
            assert "my_custom_model" in s.multimodal.models
            # 包内默认的模型也被补全
            assert "moonshot_kimi_k2.5" in s.multimodal.models
            assert "minimax_m2.5" in s.multimodal.models

    def test_search_fields_added_to_old_config(self, monkeypatch):
        """老配置中没有 search_api_key 等字段时，被补全"""
        from aacode.config import Settings
        old_config = {
            "model": {"name": "old-model"},
            "tools": {"enable_shell": True},
        }
        with tempfile.TemporaryDirectory() as td:
            monkeypatch.setattr(pd, "user_config_dir", lambda *a, **kw: str(td))
            cfg_path = Path(td) / "aacode_config.yaml"
            cfg_path.parent.mkdir(parents=True, exist_ok=True)
            cfg_path.write_text(yaml.dump(old_config), encoding="utf-8")

            s = Settings()
            assert s.tools.search_api_url is not None  # 包内默认有 search_api_url
            assert hasattr(s.tools, "search_api_key")
            assert hasattr(s.tools, "search_engine")

            saved = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
            assert "search_api_url" in saved["tools"]
            assert "search_engine" in saved["tools"]
