"""
test_done_protocol.py

.done 프로토콜 수정 검증 테스트 (task-616.1 설계 반영)

테스트 항목:
- log_protocol() 단위 테스트
- 통합 테스트 시나리오 (새 설계: send_telegram_notification 기반)
- done-watcher 로직 시뮬레이션 (새 설계: escalation/acked 기반)
"""

import json
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# scripts 디렉토리를 import path에 추가
_SCRIPTS_DIR = Path(__file__).parent.parent

# notify-completion.py는 하이픈이 있으므로 importlib으로 임포트
import importlib.util

_MODULE_PATH = _SCRIPTS_DIR / "notify-completion.py"
spec = importlib.util.spec_from_file_location("notify_completion", _MODULE_PATH)
assert spec is not None
notify_completion = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(notify_completion)


class TestLogProtocol:
    """log_protocol() 함수 단위 테스트"""

    def test_creates_log_file(self, tmp_path: Path) -> None:
        """로그 파일 자동 생성"""
        log_path = tmp_path / "done-protocol.log"
        assert not log_path.exists()
        with patch.object(notify_completion, "DONE_PROTOCOL_LOG", str(log_path)):
            notify_completion.log_protocol("task-010.1", "test message")
        assert log_path.exists()

    def test_log_format(self, tmp_path: Path) -> None:
        """[ISO8601] [notify-completion] task_id: message 포맷"""
        log_path = tmp_path / "done-protocol.log"
        with patch.object(notify_completion, "DONE_PROTOCOL_LOG", str(log_path)):
            notify_completion.log_protocol("task-011.1", "상태 전이 완료")
        content = log_path.read_text(encoding="utf-8")
        # ISO8601 타임스탬프: [2026-...T...]
        assert "[notify-completion]" in content
        assert "task-011.1" in content
        assert "상태 전이 완료" in content
        # 형식: [타임스탬프] [notify-completion] task_id: message
        line = content.strip()
        assert line.startswith("[")
        assert "] [notify-completion] task-011.1: 상태 전이 완료" in line

    def test_appends_to_existing_log(self, tmp_path: Path) -> None:
        """기존 로그에 추가 (덮어쓰기 안 함)"""
        log_path = tmp_path / "done-protocol.log"
        log_path.write_text("[기존 로그 항목]\n", encoding="utf-8")
        with patch.object(notify_completion, "DONE_PROTOCOL_LOG", str(log_path)):
            notify_completion.log_protocol("task-012.1", "새 항목")
        content = log_path.read_text(encoding="utf-8")
        assert "[기존 로그 항목]" in content
        assert "task-012.1" in content
        lines = [l for l in content.splitlines() if l.strip()]
        assert len(lines) == 2


class TestDoneProtocolIntegration:
    """통합 테스트 시나리오 (새 설계: .done.notified 없음)"""

    @patch("subprocess.run")
    def test_normal_flow_sends_telegram_only(self, mock_run: MagicMock, tmp_path: Path) -> None:
        """정상 플로우: send_telegram_notification 호출, wake_anu_session 미호출"""

        def side_effect(cmd: object, *args: object, **kwargs: object) -> MagicMock:
            result = MagicMock()
            result.returncode = 0
            result.stderr = ""
            if isinstance(cmd, list):
                cmd_str = " ".join(str(c) for c in cmd)
                if "chain_manager.py" in cmd_str and "check" in cmd_str:
                    result.stdout = json.dumps(
                        {"in_chain": False, "is_last": False, "chain_id": None, "next_task_id": None}
                    )
                else:
                    result.stdout = json.dumps({"status": "ok"})
            else:
                result.stdout = json.dumps({"status": "ok"})
            return result

        mock_run.side_effect = side_effect
        (tmp_path / "memory" / "events").mkdir(parents=True, exist_ok=True)

        with (
            patch("sys.argv", ["notify-completion.py", "task-030.1"]),
            patch.dict("os.environ", {"COKACDIR_KEY_ANU": "test-key"}),
            patch.object(notify_completion, "WORKSPACE_ROOT", str(tmp_path)),
        ):
            notify_completion.main()

        # cokacdir 호출은 있어야 함 (텔레그램 전송)
        cokacdir_calls = [c for c in mock_run.call_args_list if isinstance(c[0][0], list) and c[0][0][0] == "cokacdir"]
        assert len(cokacdir_calls) >= 1, "send_telegram_notification(cokacdir)가 호출되어야 함"

        # 메시지에 "✅ {task_id} 완료" 포함 확인
        cokacdir_cmd = cokacdir_calls[0][0][0]
        cokacdir_cmd_str = " ".join(str(x) for x in cokacdir_cmd)
        assert "task-030.1" in cokacdir_cmd_str, "메시지에 task_id가 포함되어야 함"
        assert "완료" in cokacdir_cmd_str, "메시지에 '완료'가 포함되어야 함"

        # wake_anu_session 호출 방식 확인: --cron 뒤 메시지가 build_prompt 형식이 아닌지
        # (즉, "done.clear", "done.notified" 같은 구형 프롬프트 아님)
        cron_idx = cokacdir_cmd.index("--cron") if "--cron" in cokacdir_cmd else -1
        if cron_idx >= 0 and cron_idx + 1 < len(cokacdir_cmd):
            msg = cokacdir_cmd[cron_idx + 1]
            assert "done.clear" not in msg, "wake_anu_session의 구형 프롬프트가 전송되면 안 됨"

        # .done.notified 파일이 생성되지 않아야 함
        events_dir = tmp_path / "memory" / "events"
        assert not (events_dir / "task-030.1.done.notified").exists(), ".done.notified 파일이 생성되면 안 됨"

    @patch("subprocess.run")
    def test_chain_middle_dispatches_and_sends_telegram(self, mock_run: MagicMock, tmp_path: Path) -> None:
        """체인 중간: dispatch(bash) 호출, 텔레그램 알림 전송, .done.notified 미생성"""

        def side_effect(cmd: object, *args: object, **kwargs: object) -> MagicMock:
            result = MagicMock()
            result.returncode = 0
            result.stderr = ""
            if isinstance(cmd, list):
                cmd_str = " ".join(str(c) for c in cmd)
                if "chain_manager.py" in cmd_str and "check" in cmd_str:
                    result.stdout = json.dumps(
                        {
                            "in_chain": True,
                            "is_last": False,
                            "chain_id": "chain-031",
                            "next_task_id": "task-031.2",
                        }
                    )
                elif "chain_manager.py" in cmd_str and "next" in cmd_str:
                    result.stdout = json.dumps(
                        {
                            "action": "dispatch",
                            "task_file": "memory/tasks/task-031.2.md",
                            "team": "dev1-team",
                            "task_id": "task-031.2",
                        }
                    )
                elif "bash" in cmd_str:
                    result.stdout = json.dumps({"status": "dispatched"})
                elif "cokacdir" in cmd_str:
                    result.stdout = json.dumps({"status": "ok"})
                else:
                    result.stdout = json.dumps({"status": "ok"})
            else:
                result.stdout = json.dumps({"status": "ok"})
            return result

        mock_run.side_effect = side_effect
        (tmp_path / "memory" / "events").mkdir(parents=True, exist_ok=True)

        with (
            patch("sys.argv", ["notify-completion.py", "task-031.1"]),
            patch.dict("os.environ", {"COKACDIR_KEY_ANU": "test-key"}),
            patch.object(notify_completion, "WORKSPACE_ROOT", str(tmp_path)),
        ):
            notify_completion.main()

        events_dir = tmp_path / "memory" / "events"

        # .done.notified 파일이 생성되지 않아야 함
        assert not (events_dir / "task-031.1.done.notified").exists(), "체인 중간에서 .done.notified 생성 금지"

        # dispatch(bash) 호출 확인
        bash_calls = [
            c
            for c in mock_run.call_args_list
            if isinstance(c[0][0], list) and len(c[0][0]) >= 2 and "bash" in str(c[0][0][0])
        ]
        assert len(bash_calls) >= 1, "dispatch(bash -c ...) 호출이 있어야 함"

        # 텔레그램 알림(cokacdir) 호출 확인
        cokacdir_calls = [c for c in mock_run.call_args_list if isinstance(c[0][0], list) and c[0][0][0] == "cokacdir"]
        assert len(cokacdir_calls) >= 1, "체인 중간에서도 텔레그램 알림이 전송되어야 함"

    def test_done_file_survives_notification(self, tmp_path: Path) -> None:
        """.done 파일은 notification 후에도 events/에 유지"""

        def side_effect(cmd: object, *args: object, **kwargs: object) -> MagicMock:
            result = MagicMock()
            result.returncode = 0
            result.stderr = ""
            if isinstance(cmd, list):
                cmd_str = " ".join(str(c) for c in cmd)
                if "chain_manager.py" in cmd_str and "check" in cmd_str:
                    result.stdout = json.dumps(
                        {"in_chain": False, "is_last": False, "chain_id": None, "next_task_id": None}
                    )
                else:
                    result.stdout = json.dumps({"status": "ok"})
            else:
                result.stdout = json.dumps({"status": "ok"})
            return result

        events_dir = tmp_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)
        done_path = events_dir / "task-033.1.done"
        done_path.write_text(
            json.dumps({"task_id": "task-033.1", "status": "completed"}),
            encoding="utf-8",
        )

        with (
            patch("subprocess.run", side_effect=side_effect),
            patch("sys.argv", ["notify-completion.py", "task-033.1"]),
            patch.dict("os.environ", {"COKACDIR_KEY_ANU": "test-key"}),
            patch.object(notify_completion, "WORKSPACE_ROOT", str(tmp_path)),
        ):
            notify_completion.main()

        # .done 파일이 events/에 남아있어야 함 (archive로 이동 안 됨, rename 금지)
        assert done_path.exists(), ".done 파일이 notification 후에도 events/에 유지되어야 함"

    @patch("subprocess.run")
    def test_done_file_not_modified_after_notification(self, mock_run: MagicMock, tmp_path: Path) -> None:
        """.done 파일이 rename/삭제되지 않는지 확인"""

        def side_effect(cmd: object, *args: object, **kwargs: object) -> MagicMock:
            result = MagicMock()
            result.returncode = 0
            result.stderr = ""
            if isinstance(cmd, list):
                cmd_str = " ".join(str(c) for c in cmd)
                if "chain_manager.py" in cmd_str and "check" in cmd_str:
                    result.stdout = json.dumps(
                        {"in_chain": False, "is_last": False, "chain_id": None, "next_task_id": None}
                    )
                else:
                    result.stdout = json.dumps({"status": "ok"})
            else:
                result.stdout = json.dumps({"status": "ok"})
            return result

        mock_run.side_effect = side_effect

        events_dir = tmp_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)
        done_path = events_dir / "task-034.1.done"
        original_content = json.dumps({"task_id": "task-034.1", "status": "completed"})
        done_path.write_text(original_content, encoding="utf-8")

        with (
            patch("sys.argv", ["notify-completion.py", "task-034.1"]),
            patch.dict("os.environ", {"COKACDIR_KEY_ANU": "test-key"}),
            patch.object(notify_completion, "WORKSPACE_ROOT", str(tmp_path)),
        ):
            notify_completion.main()

        # .done 파일이 그대로 존재해야 함
        assert done_path.exists(), ".done 파일이 삭제되면 안 됨"
        # 내용도 변경되면 안 됨
        assert done_path.read_text(encoding="utf-8") == original_content, ".done 파일 내용이 변경되면 안 됨"
        # .done.clear로 rename된 파일이 없어야 함
        assert not (events_dir / "task-034.1.done.clear").exists(), ".done.clear 파일이 생성되면 안 됨"

    @patch("subprocess.run")
    def test_no_done_notified_created(self, mock_run: MagicMock, tmp_path: Path) -> None:
        """.done.notified 파일이 절대 생성되지 않는지 확인"""

        def side_effect(cmd: object, *args: object, **kwargs: object) -> MagicMock:
            result = MagicMock()
            result.returncode = 0
            result.stderr = ""
            if isinstance(cmd, list):
                cmd_str = " ".join(str(c) for c in cmd)
                if "chain_manager.py" in cmd_str and "check" in cmd_str:
                    result.stdout = json.dumps(
                        {"in_chain": False, "is_last": False, "chain_id": None, "next_task_id": None}
                    )
                else:
                    result.stdout = json.dumps({"status": "ok"})
            else:
                result.stdout = json.dumps({"status": "ok"})
            return result

        mock_run.side_effect = side_effect

        events_dir = tmp_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)

        with (
            patch("sys.argv", ["notify-completion.py", "task-035.1"]),
            patch.dict("os.environ", {"COKACDIR_KEY_ANU": "test-key"}),
            patch.object(notify_completion, "WORKSPACE_ROOT", str(tmp_path)),
        ):
            notify_completion.main()

        # .done.notified 파일이 생성되지 않아야 함
        assert not (events_dir / "task-035.1.done.notified").exists(), ".done.notified 파일이 생성되면 안 됨"

    @patch("subprocess.run")
    def test_independent_task_sends_telegram_not_wake(self, mock_run: MagicMock, tmp_path: Path) -> None:
        """독립 작업에서 send_telegram이 호출되고 wake_anu가 호출되지 않는지"""

        def side_effect(cmd: object, *args: object, **kwargs: object) -> MagicMock:
            result = MagicMock()
            result.returncode = 0
            result.stderr = ""
            if isinstance(cmd, list):
                cmd_str = " ".join(str(c) for c in cmd)
                if "chain_manager.py" in cmd_str and "check" in cmd_str:
                    result.stdout = json.dumps(
                        {"in_chain": False, "is_last": False, "chain_id": None, "next_task_id": None}
                    )
                else:
                    result.stdout = json.dumps({"status": "ok"})
            else:
                result.stdout = json.dumps({"status": "ok"})
            return result

        mock_run.side_effect = side_effect

        events_dir = tmp_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)

        with (
            patch("sys.argv", ["notify-completion.py", "task-036.1"]),
            patch.dict("os.environ", {"COKACDIR_KEY_ANU": "test-key"}),
            patch.object(notify_completion, "WORKSPACE_ROOT", str(tmp_path)),
        ):
            notify_completion.main()

        # cokacdir 호출이 있어야 함 (send_telegram_notification)
        cokacdir_calls = [c for c in mock_run.call_args_list if isinstance(c[0][0], list) and c[0][0][0] == "cokacdir"]
        assert len(cokacdir_calls) >= 1, "독립 작업에서 send_telegram_notification이 호출되어야 함"

        # 메시지에 "✅ {task_id} 완료" 형식 포함 확인
        cokacdir_cmd = cokacdir_calls[0][0][0]
        cron_idx = cokacdir_cmd.index("--cron") if "--cron" in cokacdir_cmd else -1
        assert cron_idx >= 0, "cokacdir 명령에 --cron이 없음"
        msg = cokacdir_cmd[cron_idx + 1]
        assert "task-036.1" in msg, f"메시지에 task_id가 없음: {msg}"
        assert "완료" in msg, f"메시지에 '완료'가 없음: {msg}"

        # wake_anu_session의 구형 프롬프트 형식이 아닌지 확인
        assert "done.clear" not in msg, f"wake_anu_session 구형 프롬프트가 전송됨: {msg}"
        assert "done.notified" not in msg, f"done.notified 참조 메시지가 전송됨: {msg}"


class TestDoneWatcherLogic:
    """done-watcher 새 설계 로직 시뮬레이션 테스트 (escalation/acked 기반)"""

    def _simulate_stale_done_escalation(
        self, events_dir: Path, task_id: str, age_minutes: int, escalation_threshold: int = 30
    ) -> bool:
        """
        .done 파일이 age_minutes 경과했을 때 에스컬레이션 여부 반환.
        threshold(기본 30분) 이상이면 True, 미만이면 False.
        .done.escalated가 이미 존재하면 중복 방지 → False.
        """
        done_path = events_dir / f"{task_id}.done"
        escalated_path = events_dir / f"{task_id}.done.escalated"

        if not done_path.exists():
            return False
        if escalated_path.exists():
            return False
        return age_minutes >= escalation_threshold

    def _simulate_acked_cleanup(self, events_dir: Path, archive_dir: Path, task_id: str, acked_age_hours: int) -> bool:
        """
        .done.acked가 acked_age_hours 이상이면 archive로 이동. 이동 여부 반환.
        """
        acked_path = events_dir / f"{task_id}.done.acked"
        if not acked_path.exists():
            return False
        if acked_age_hours >= 24:
            archive_dir.mkdir(parents=True, exist_ok=True)
            acked_path.rename(archive_dir / f"{task_id}.done.acked")
            return True
        return False

    def test_stale_done_escalation(self, tmp_path: Path) -> None:
        """.done 파일 30분 이상 → 에스컬레이션"""
        events_dir = tmp_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)

        task_id = "task-050.1"
        (events_dir / f"{task_id}.done").write_text("{}", encoding="utf-8")

        # 30분 이상 → 에스컬레이션
        assert self._simulate_stale_done_escalation(events_dir, task_id, age_minutes=30) is True
        assert self._simulate_stale_done_escalation(events_dir, task_id, age_minutes=60) is True

    def test_stale_done_under_30min_no_action(self, tmp_path: Path) -> None:
        """.done 파일 30분 미만 → 아무 동작 없음"""
        events_dir = tmp_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)

        task_id = "task-051.1"
        (events_dir / f"{task_id}.done").write_text("{}", encoding="utf-8")

        # 29분 → 에스컬레이션 없음
        assert self._simulate_stale_done_escalation(events_dir, task_id, age_minutes=29) is False
        assert self._simulate_stale_done_escalation(events_dir, task_id, age_minutes=0) is False

    def test_escalated_marker_prevents_duplicate(self, tmp_path: Path) -> None:
        """.done.escalated 존재 시 중복 알림 없음"""
        events_dir = tmp_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)

        task_id = "task-052.1"
        (events_dir / f"{task_id}.done").write_text("{}", encoding="utf-8")
        (events_dir / f"{task_id}.done.escalated").write_text("{}", encoding="utf-8")

        # .done.escalated 존재 → 에스컬레이션 중복 방지
        assert self._simulate_stale_done_escalation(events_dir, task_id, age_minutes=60) is False

    def test_acked_cleanup_after_24h(self, tmp_path: Path) -> None:
        """.done.acked 24시간 이상 → archive 이동"""
        events_dir = tmp_path / "memory" / "events"
        events_dir.mkdir(parents=True, exist_ok=True)
        archive_dir = tmp_path / "memory" / "archive"

        task_id = "task-053.1"
        (events_dir / f"{task_id}.done.acked").write_text("{}", encoding="utf-8")

        # 24시간 미만 → 이동 없음
        moved = self._simulate_acked_cleanup(events_dir, archive_dir, task_id, acked_age_hours=23)
        assert moved is False
        assert (events_dir / f"{task_id}.done.acked").exists(), "24시간 미만에는 archive 이동 없어야 함"

        # 24시간 이상 → archive 이동
        moved = self._simulate_acked_cleanup(events_dir, archive_dir, task_id, acked_age_hours=24)
        assert moved is True
        assert not (events_dir / f"{task_id}.done.acked").exists(), "24시간 이상이면 events/에서 제거되어야 함"
        assert (archive_dir / f"{task_id}.done.acked").exists(), "archive/로 이동되어야 함"
