"""
test_bot_settings_sync.py

task-1441.1: _sync_bot_settings()와 ConfigManager.reload() / _sync_check() 단위 테스트.

작성자: 토트 (Thoth) — dev8-team QA
"""

import json
import sys
import types
from pathlib import Path
from unittest.mock import patch

import pytest

# ---------------------------------------------------------------------------
# 경로 설정: workspace 루트를 sys.path에 추가
# ---------------------------------------------------------------------------

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

# ---------------------------------------------------------------------------
# 공통 상수 / 헬퍼
# ---------------------------------------------------------------------------

_WORKSPACE = Path("/home/jay/workspace")

_FAKE_CONSTANTS: dict = {
    "meta": {"version": "1.0", "last_updated": "2026-04-04"},
    "chat_id": "6937032012",
    "cokacdir_key": "$COKACDIR_KEY",
    "teams": {"dev1-team": "dev1"},
    "bots": {
        "dev1": {
            "display_name": "dev1_Hermes_bot",
            "username": "dev1_hermes_bot",
            "team_dir": "/home/jay/workspace/teams/dev1",
            "model": "claude-opus-4-6",
        }
    },
    "team_to_bot": {},
    "work_levels": {},
    "thresholds": {},
}

_FAKE_BOT_SETTINGS: dict = {
    "abc123": {
        "display_name": "dev9_TestBot_bot",
        "username": "dev9_testbot_bot",
        "models": {"6937032012": "claude-opus-4-6"},
        "last_sessions": {"6937032012": "/home/jay/workspace/teams/dev9"},
        "token": "secret-token-123",
    }
}


def _write_json(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def _read_json(path: Path) -> dict:
    with open(path, encoding="utf-8") as f:
        return json.load(f)


def _make_workspace(tmp_path: Path) -> tuple[Path, Path, Path]:
    """tmp_path 아래에 workspace 뼈대를 만들고 (workspace, config_dir, memory_dir) 반환."""
    config_dir = tmp_path / "config"
    memory_dir = tmp_path / "memory"
    config_dir.mkdir(parents=True, exist_ok=True)
    memory_dir.mkdir(parents=True, exist_ok=True)
    return tmp_path, config_dir, memory_dir


# ---------------------------------------------------------------------------
# dispatch 모듈을 격리 WORKSPACE로 임포트하는 헬퍼
# ---------------------------------------------------------------------------


def _import_dispatch(tmp_path: Path, monkeypatch=None) -> types.ModuleType:
    """dispatch 모듈을 캐시에서 제거한 뒤 재임포트하고 WORKSPACE를 패치한다."""
    if str(_WORKSPACE) not in sys.path:
        sys.path.insert(0, str(_WORKSPACE))

    # 의존 모듈 사전 임포트 (dispatch.py의 try/except 블록 통과)
    try:
        import prompts.team_prompts  # noqa: F401
    except ImportError:
        pass

    # 기존 dispatch 모듈을 보존 (다른 테스트의 함수 __globals__ 보호)
    _original_dispatch = sys.modules.get("dispatch")

    # monkeypatch가 있으면 sys.modules["dispatch"] 복원을 monkeypatch에 위임
    if monkeypatch is not None and _original_dispatch is not None:
        monkeypatch.setitem(sys.modules, "dispatch", _original_dispatch)

    for mod_name in list(sys.modules.keys()):
        if mod_name == "dispatch":
            del sys.modules[mod_name]

    import dispatch as _dispatch  # type: ignore[import]

    if monkeypatch is not None:
        monkeypatch.setattr(_dispatch, "WORKSPACE", tmp_path)
    else:
        _dispatch.WORKSPACE = tmp_path
    return _dispatch


# ---------------------------------------------------------------------------
# Test 1: _sync_bot_settings()가 constants.json에 새 봇 항목을 추가한다
# ---------------------------------------------------------------------------


class TestSyncUpdateConstantsJson:
    """bot_settings.json의 새 봇이 constants.json[bots]에 반영되는지 검증."""

    def test_sync_updates_constants_json(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        _, config_dir, _ = _make_workspace(tmp_path)

        # constants.json 초기 상태: dev9 봇 없음
        constants_path = config_dir / "constants.json"
        _write_json(constants_path, _FAKE_CONSTANTS)

        # bot_settings.json: dev9 가짜 봇
        fake_home = tmp_path / "fake_home"
        cokacdir_dir = fake_home / ".cokacdir"
        cokacdir_dir.mkdir(parents=True, exist_ok=True)
        settings_path = cokacdir_dir / "bot_settings.json"
        _write_json(settings_path, _FAKE_BOT_SETTINGS)

        dispatch_mod = _import_dispatch(tmp_path, monkeypatch)

        # Path.home()을 패치하여 가짜 홈 디렉터리 반환
        monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))

        dispatch_mod._sync_bot_settings()

        updated = _read_json(constants_path)
        assert "dev9" in updated.get("bots", {}), "dev9 봇이 constants.json[bots]에 추가되어야 한다"

        dev9_entry = updated["bots"]["dev9"]
        assert dev9_entry["display_name"] == "dev9_TestBot_bot"
        assert dev9_entry["username"] == "dev9_testbot_bot"
        assert dev9_entry["model"] == "claude-opus-4-6"
        assert dev9_entry["team_dir"] == "/home/jay/workspace/teams/dev9"


# ---------------------------------------------------------------------------
# Test 2: _sync_bot_settings()가 기존 봇 항목을 삭제하지 않는다 (가산적)
# ---------------------------------------------------------------------------


class TestSyncPreservesExistingBots:
    """sync 실행 후 constants.json의 기존 봇이 유지되는지 검증."""

    def test_sync_preserves_existing_bots(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        _, config_dir, _ = _make_workspace(tmp_path)

        constants_path = config_dir / "constants.json"
        _write_json(constants_path, _FAKE_CONSTANTS)  # dev1 봇 포함

        fake_home = tmp_path / "fake_home"
        cokacdir_dir = fake_home / ".cokacdir"
        cokacdir_dir.mkdir(parents=True, exist_ok=True)
        _write_json(cokacdir_dir / "bot_settings.json", _FAKE_BOT_SETTINGS)  # dev9 봇만

        dispatch_mod = _import_dispatch(tmp_path, monkeypatch)
        monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))

        dispatch_mod._sync_bot_settings()

        updated = _read_json(constants_path)
        bots = updated.get("bots", {})

        assert "dev1" in bots, "기존 dev1 봇은 삭제되어서는 안 된다"
        assert "dev9" in bots, "새로 추가된 dev9 봇도 있어야 한다"


# ---------------------------------------------------------------------------
# Test 3: bot_settings.json이 없을 때 _sync_bot_settings()는 크래시하지 않는다
# ---------------------------------------------------------------------------


class TestSyncSkipsWithoutBotSettings:
    """bot_settings.json 부재 시 함수가 조용히 종료되고 constants.json이 불변임을 검증."""

    def test_sync_skips_without_bot_settings(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        _, config_dir, _ = _make_workspace(tmp_path)

        constants_path = config_dir / "constants.json"
        _write_json(constants_path, _FAKE_CONSTANTS)

        # bot_settings.json 미생성 (빈 .cokacdir 디렉터리)
        fake_home = tmp_path / "fake_home"
        (fake_home / ".cokacdir").mkdir(parents=True, exist_ok=True)

        dispatch_mod = _import_dispatch(tmp_path, monkeypatch)
        monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))

        # 예외 없이 실행되어야 한다
        dispatch_mod._sync_bot_settings()

        # constants.json은 변경되어서는 안 된다
        unchanged = _read_json(constants_path)
        assert unchanged == _FAKE_CONSTANTS, "bot_settings.json 없을 때 constants.json은 변경되지 않아야 한다"


# ---------------------------------------------------------------------------
# Test 4: ConfigManager.reload()가 변경된 constants.json을 재로드한다
# ---------------------------------------------------------------------------


class TestReloadMethod:
    """ConfigManager.reload() 호출 후 수정된 상수가 반영되는지 검증."""

    def test_reload_method(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        from config.loader import ConfigManager

        # 테스트 격리: 싱글톤 초기화
        ConfigManager.reset()

        # config.loader가 사용하는 CONFIG_DIR를 tmp_path/config로 교체
        config_dir = tmp_path / "config"
        config_dir.mkdir(parents=True, exist_ok=True)

        # 기존 실제 config 파일들을 복사 (loader가 모든 파일을 로드하므로)
        real_config_dir = _WORKSPACE / "config"
        import shutil

        for fname in ("paths.json", "design-system.json", "module-registry.json"):
            shutil.copy(real_config_dir / fname, config_dir / fname)

        # constants.json: 초기 상태 (dev9 없음)
        initial_constants = {k: v for k, v in _FAKE_CONSTANTS.items()}
        _write_json(config_dir / "constants.json", initial_constants)

        monkeypatch.setattr("config.loader.CONFIG_DIR", config_dir)

        # 싱글톤 초기화 후 새 인스턴스 생성
        ConfigManager.reset()
        cfg = ConfigManager.get_instance()

        assert "dev9" not in cfg.constants.get("bots", {}), "초기 상태에는 dev9 없어야 함"

        # constants.json에 dev9 추가 후 reload()
        updated_constants = {**initial_constants}
        updated_constants.setdefault("bots", {})["dev9"] = {
            "display_name": "dev9_TestBot_bot",
            "username": "dev9_testbot_bot",
            "team_dir": "/home/jay/workspace/teams/dev9",
            "model": "claude-opus-4-6",
        }
        _write_json(config_dir / "constants.json", updated_constants)

        cfg.reload()

        assert "dev9" in cfg.constants.get("bots", {}), "reload() 후 dev9 봇이 constants에 반영되어야 한다"

        # 테스트 후 싱글톤 정리
        ConfigManager.reset()


# ---------------------------------------------------------------------------
# Test 5: _sync_check()가 성공적인 동기화 후 0을 반환한다
# ---------------------------------------------------------------------------


class TestSyncCheckOk:
    """_sync_bot_settings() 후 _sync_check()가 0을 반환하는지 검증."""

    def test_sync_check_ok(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        _, config_dir, memory_dir = _make_workspace(tmp_path)

        constants_path = config_dir / "constants.json"
        _write_json(constants_path, _FAKE_CONSTANTS)

        fake_home = tmp_path / "fake_home"
        cokacdir_dir = fake_home / ".cokacdir"
        cokacdir_dir.mkdir(parents=True, exist_ok=True)
        _write_json(cokacdir_dir / "bot_settings.json", _FAKE_BOT_SETTINGS)

        dispatch_mod = _import_dispatch(tmp_path, monkeypatch)
        monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))

        # sync 실행: constants.json에 dev9 반영 + bot_settings_sync.json 생성
        dispatch_mod._sync_bot_settings()

        # _sync_check()가 참조하는 경로를 패치
        from config import loader as loader_mod

        monkeypatch.setattr(loader_mod, "CONFIG_DIR", config_dir)

        # _sync_check는 workspace_root = Path(__file__).parent.parent를 사용하므로
        # 해당 경로 계산을 패치한다
        sync_path = memory_dir / "bot_settings_sync.json"
        assert sync_path.exists(), "bot_settings_sync.json이 생성되어야 한다"

        with patch.object(loader_mod.Path, "__new__") as _:
            # 경로 패치 대신 직접 _sync_check 내부 경로 변수를 monkeypatch로 우회
            pass

        # _sync_check() 내부에서 workspace_root = Path(__file__).parent.parent를 사용하므로
        # config/loader.py의 __file__은 실제 경로이다.
        # 따라서 sync_path를 실제 memory 경로로 심링크/복사한다.
        real_memory_dir = _WORKSPACE / "memory"
        real_sync_path = real_memory_dir / "bot_settings_sync.json"

        # 테스트용 bot_settings_sync.json을 실제 경로에 임시 배치
        # (이미 존재하는 경우 내용을 백업 후 복원)
        backup: dict | None = None
        if real_sync_path.exists():
            backup = _read_json(real_sync_path)

        try:
            _write_json(real_sync_path, _read_json(sync_path))

            # constants.json도 실제 config 경로 대신 패치
            with patch.object(loader_mod, "CONFIG_DIR", config_dir):
                result = loader_mod._sync_check()

            assert result == 0, f"_sync_check()는 동기화 완료 후 0을 반환해야 한다, 실제: {result}"
        finally:
            # 백업 복원
            if backup is not None:
                _write_json(real_sync_path, backup)
            elif real_sync_path.exists():
                real_sync_path.unlink()


# ---------------------------------------------------------------------------
# Test 6: _sync_check()가 불일치를 감지하면 1을 반환한다
# ---------------------------------------------------------------------------


class TestSyncCheckDetectsMismatch:
    """bot_settings_sync.json에 constants.json에 없는 봇이 있으면 _sync_check()가 1을 반환하는지 검증."""

    def test_sync_check_detects_mismatch(self, tmp_path: Path) -> None:
        _, config_dir, memory_dir = _make_workspace(tmp_path)

        # constants.json: dev9 봇 없음
        constants_path = config_dir / "constants.json"
        _write_json(constants_path, _FAKE_CONSTANTS)

        # bot_settings_sync.json: dev9 봇 포함 (constants에는 없음)
        sync_data = {
            "abc123": {
                "display_name": "dev9_TestBot_bot",
                "username": "dev9_testbot_bot",
                "models": {"6937032012": "claude-opus-4-6"},
                "last_sessions": {"6937032012": "/home/jay/workspace/teams/dev9"},
                # token은 마스킹된 상태
                "token": "***REDACTED***",
            }
        }
        sync_path = memory_dir / "bot_settings_sync.json"
        _write_json(sync_path, sync_data)

        from config import loader as loader_mod

        # 실제 경로에 테스트용 파일 배치 (임시)
        real_memory_dir = _WORKSPACE / "memory"
        real_sync_path = real_memory_dir / "bot_settings_sync.json"

        backup: dict | None = None
        if real_sync_path.exists():
            backup = _read_json(real_sync_path)

        try:
            _write_json(real_sync_path, sync_data)

            with patch.object(loader_mod, "CONFIG_DIR", config_dir):
                result = loader_mod._sync_check()

            assert result == 1, (
                "_sync_check()는 bot_settings_sync.json에 constants.json에 없는 봇이 있을 때 1을 반환해야 한다"
            )
        finally:
            if backup is not None:
                _write_json(real_sync_path, backup)
            elif real_sync_path.exists():
                real_sync_path.unlink()
