"""
test_notify_completion.py — notify-completion.py 테스트

테스트 항목:
- TestGetAnuKey: 환경변수 있을 때 / 없을 때 (sys.exit(1))
- TestCheckChainStatus: 정상 응답, 타임아웃, 비정상 응답
- TestLogProtocol: done-protocol.log 기록 함수 테스트
- TestSendTelegramNotification: cokacdir subprocess 호출 확인
- TestMain: 체인 중간 Phase / 마지막 Phase / 일반 작업 분기
- TestDispatchExecution: dispatch.py subprocess 호출 통합 테스트
"""

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

_TEST_CHAT_ID = os.environ.get("COKACDIR_CHAT_ID", "6937032012")

# notify-completion.py가 scripts/ 디렉토리에 있으므로 경로 추가
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))

import importlib.util

# notify-completion.py는 하이픈이 있어 직접 import 불가 → importlib 사용
_spec = importlib.util.spec_from_file_location(
    "notify_completion",
    str(Path(__file__).parent.parent / "scripts" / "notify-completion.py"),
)
_mod = importlib.util.module_from_spec(_spec)  # type: ignore[arg-type]
_spec.loader.exec_module(_mod)  # type: ignore[union-attr]

get_anu_key = _mod.get_anu_key
check_chain_status = _mod.check_chain_status
log_protocol = _mod.log_protocol
send_telegram_notification = _mod.send_telegram_notification
dispatch_next_phase = _mod.dispatch_next_phase
end_task_timer = _mod.end_task_timer
main = _mod.main


class TestGetAnuKey(unittest.TestCase):
    """get_anu_key 함수 테스트"""

    def test_get_anu_key_success(self) -> None:
        """COKACDIR_KEY_ANU 환경변수 있으면 정상 반환"""
        with patch.dict(os.environ, {"COKACDIR_KEY_ANU": "test-key-abc123"}):
            result = get_anu_key()
        self.assertEqual(result, "test-key-abc123")

    def test_get_anu_key_missing_exits(self) -> None:
        """COKACDIR_KEY_ANU 환경변수 없으면 sys.exit(1) 호출"""
        env_without_key = {k: v for k, v in os.environ.items() if k != "COKACDIR_KEY_ANU"}
        with patch.dict(os.environ, env_without_key, clear=True):
            with self.assertRaises(SystemExit) as ctx:
                get_anu_key()
        self.assertEqual(ctx.exception.code, 1)

    def test_get_anu_key_empty_exits(self) -> None:
        """COKACDIR_KEY_ANU 빈 문자열이면 sys.exit(1) 호출"""
        with patch.dict(os.environ, {"COKACDIR_KEY_ANU": ""}):
            with self.assertRaises(SystemExit) as ctx:
                get_anu_key()
        self.assertEqual(ctx.exception.code, 1)


class TestCheckChainStatus(unittest.TestCase):
    """check_chain_status 함수 테스트"""

    def test_check_chain_status_success(self) -> None:
        """subprocess 성공 시 JSON 파싱 결과 반환"""
        fake_response = {
            "in_chain": True,
            "is_last": False,
            "chain_id": "chain-42",
            "next_task_id": "task-381.2",
        }
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = json.dumps(fake_response)

        with patch("subprocess.run", return_value=mock_result):
            result = check_chain_status("task-381.1")

        self.assertEqual(result["in_chain"], True)
        self.assertEqual(result["is_last"], False)
        self.assertEqual(result["chain_id"], "chain-42")
        self.assertEqual(result["next_task_id"], "task-381.2")

    def test_check_chain_status_failure_returns_default(self) -> None:
        """subprocess 실패(returncode != 0) 시 in_chain=False 기본값 반환"""
        mock_result = MagicMock()
        mock_result.returncode = 1
        mock_result.stdout = ""

        with patch("subprocess.run", return_value=mock_result):
            result = check_chain_status("task-381.1")

        self.assertFalse(result["in_chain"])
        self.assertFalse(result["is_last"])
        self.assertIsNone(result["chain_id"])
        self.assertIsNone(result["next_task_id"])

    def test_check_chain_status_timeout_returns_default(self) -> None:
        """subprocess TimeoutExpired 시 in_chain=False 기본값 반환"""
        with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="python3", timeout=30)):
            result = check_chain_status("task-381.1")

        self.assertFalse(result["in_chain"])
        self.assertFalse(result["is_last"])

    def test_check_chain_status_invalid_json_returns_default(self) -> None:
        """subprocess 성공이지만 JSON 파싱 실패 시 기본값 반환"""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "not valid json {"

        with patch("subprocess.run", return_value=mock_result):
            result = check_chain_status("task-381.1")

        self.assertFalse(result["in_chain"])
        self.assertFalse(result["is_last"])


class TestLogProtocol(unittest.TestCase):
    """log_protocol 함수 테스트"""

    def test_log_protocol_writes_to_file(self) -> None:
        """log_protocol이 DONE_PROTOCOL_LOG에 기록한다"""
        with tempfile.TemporaryDirectory() as tmpdir:
            log_path = Path(tmpdir) / "logs" / "done-protocol.log"
            with patch.object(_mod, "DONE_PROTOCOL_LOG", str(log_path)):
                log_protocol("task-999.1", "test message")
            self.assertTrue(log_path.exists())
            content = log_path.read_text(encoding="utf-8")
            self.assertIn("task-999.1", content)
            self.assertIn("test message", content)

    def test_log_protocol_creates_parent_dirs(self) -> None:
        """logs 디렉토리가 없어도 자동 생성"""
        with tempfile.TemporaryDirectory() as tmpdir:
            log_path = Path(tmpdir) / "nonexistent" / "subdir" / "done-protocol.log"
            with patch.object(_mod, "DONE_PROTOCOL_LOG", str(log_path)):
                log_protocol("task-888.1", "dir creation test")
            self.assertTrue(log_path.exists())

    def test_log_protocol_silent_on_oserror(self) -> None:
        """OSError 발생 시 예외를 무시하고 정상 반환"""
        with patch("builtins.open", side_effect=OSError("permission denied")):
            # 예외가 발생하지 않아야 함
            try:
                log_protocol("task-777.1", "error test")
            except OSError:
                self.fail("log_protocol raised OSError")


class TestSendTelegramNotification(unittest.TestCase):
    """send_telegram_notification 함수 테스트 (cokacdir subprocess 호출)"""

    def test_send_telegram_notification_calls_subprocess(self) -> None:
        """requests.post 호출 확인"""
        mock_response = MagicMock()
        mock_response.status_code = 200
        with patch.dict(os.environ, {"ANU_BOT_TOKEN": "test-token"}):
            with patch("requests.post", return_value=mock_response) as mock_post:
                result = send_telegram_notification(_TEST_CHAT_ID, "테스트 메시지")
        mock_post.assert_called_once()
        self.assertTrue(result)

    def test_send_telegram_notification_no_exit_on_failure(self) -> None:
        """requests.post 실패 시에도 sys.exit 없이 정상 반환"""
        mock_response = MagicMock()
        mock_response.status_code = 500
        with patch.dict(os.environ, {"ANU_BOT_TOKEN": "test-token"}):
            with patch("requests.post", return_value=mock_response):
                try:
                    send_telegram_notification(_TEST_CHAT_ID, "메시지")
                except SystemExit:
                    self.fail("send_telegram_notification raised SystemExit on failure")


class TestMain(unittest.TestCase):
    """main 함수 분기 테스트"""

    def _run_main(self, argv: list, env: dict | None = None) -> None:
        """main()을 지정 argv와 환경변수로 실행하는 헬퍼"""
        base_env = {"COKACDIR_KEY_ANU": "test-key", "COKACDIR_CHAT_ID": _TEST_CHAT_ID}
        if env:
            base_env.update(env)
        with patch.dict(os.environ, base_env):
            with patch("sys.argv", ["notify-completion.py"] + argv):
                main()

    def test_main_chain_mid_phase(self) -> None:
        """체인 중간 Phase → dispatch + send_telegram_notification 호출됨"""
        chain_info = {
            "in_chain": True,
            "is_last": False,
            "chain_id": "chain-42",
            "next_task_id": "task-381.2",
        }
        dispatch_result = {
            "action": "dispatch",
            "task_file": "/tmp/test.md",
            "team": "dev1-team",
            "level": "normal",
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            events_dir = Path(tmpdir) / "memory" / "events"
            events_dir.mkdir(parents=True, exist_ok=True)

            with (
                patch.object(_mod, "WORKSPACE_ROOT", tmpdir),
                patch.object(_mod, "check_chain_status", return_value=chain_info),
                patch.object(_mod, "dispatch_next_phase", return_value=dispatch_result),
                patch.object(_mod, "send_telegram_notification") as mock_send,
                patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="", stderr="")) as mock_run,
            ):
                self._run_main(["task-381.1"])

                # send_telegram_notification 호출됨
                mock_send.assert_called_once()
                # dispatch.py subprocess 호출됨
                dispatch_calls = [c for c in mock_run.call_args_list if any("dispatch.py" in str(arg) for arg in c[0])]
                self.assertTrue(len(dispatch_calls) > 0)

    def test_main_chain_last_phase(self) -> None:
        """마지막 Phase → send_telegram_notification 호출됨"""
        chain_info = {
            "in_chain": True,
            "is_last": True,
            "chain_id": "chain-42",
            "next_task_id": None,
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            with (
                patch.object(_mod, "WORKSPACE_ROOT", tmpdir),
                patch.object(_mod, "check_chain_status", return_value=chain_info),
                patch.object(_mod, "send_telegram_notification") as mock_send,
            ):
                self._run_main(["task-381.3"])
                mock_send.assert_called_once()
                call_args = mock_send.call_args[0]
                # 메시지에 task_id 포함
                self.assertIn("task-381.3", call_args[1])

    def test_main_not_in_chain(self) -> None:
        """일반 작업(체인 아님) → send_telegram_notification 호출됨"""
        chain_info = {
            "in_chain": False,
            "is_last": False,
            "chain_id": None,
            "next_task_id": None,
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            with (
                patch.object(_mod, "WORKSPACE_ROOT", tmpdir),
                patch.object(_mod, "check_chain_status", return_value=chain_info),
                patch.object(_mod, "send_telegram_notification") as mock_send,
            ):
                self._run_main(["task-999"])
                mock_send.assert_called_once()

    def test_main_uses_cli_anu_key(self) -> None:
        """--anu-key CLI 인자로 키를 직접 전달해도 정상 동작"""
        chain_info = {"in_chain": False, "is_last": False, "chain_id": None, "next_task_id": None}

        with tempfile.TemporaryDirectory() as tmpdir:
            with (
                patch.object(_mod, "WORKSPACE_ROOT", tmpdir),
                patch.object(_mod, "check_chain_status", return_value=chain_info),
                patch.object(_mod, "send_telegram_notification") as mock_send,
            ):
                with patch.dict(
                    os.environ,
                    {"COKACDIR_KEY_ANU": "env-key", "COKACDIR_CHAT_ID": _TEST_CHAT_ID},
                ):
                    with patch("sys.argv", ["notify-completion.py", "task-123", "--anu-key", "cli-key"]):
                        main()
                mock_send.assert_called_once()

    def test_main_missing_anu_key_does_not_exit(self) -> None:
        """COKACDIR_KEY_ANU 없어도 sys.exit 없이 정상 실행"""
        chain_info = {"in_chain": False, "is_last": False, "chain_id": None, "next_task_id": None}

        with tempfile.TemporaryDirectory() as tmpdir:
            events_dir = Path(tmpdir) / "memory" / "events"
            events_dir.mkdir(parents=True, exist_ok=True)
            env_without_key = {k: v for k, v in os.environ.items() if k != "COKACDIR_KEY_ANU"}
            env_without_key["COKACDIR_CHAT_ID"] = _TEST_CHAT_ID

            with (
                patch.object(_mod, "WORKSPACE_ROOT", tmpdir),
                patch.dict(os.environ, env_without_key, clear=True),
                patch.object(_mod, "check_chain_status", return_value=chain_info),
                patch.object(_mod, "send_telegram_notification", return_value=False),
            ):
                with patch("sys.argv", ["notify-completion.py", "task-123"]):
                    # main()이 예외 없이 완료됨
                    main()


class TestDispatchExecution(unittest.TestCase):
    """dispatch.py 실제 호출 통합 테스트"""

    def test_mid_chain_dispatch_calls_subprocess(self) -> None:
        """chain 중간 Phase, action=dispatch → dispatch.py가 subprocess로 호출됨"""
        chain_info = {
            "in_chain": True,
            "is_last": False,
            "chain_id": "chain-42",
            "next_task_id": "task-381.2",
        }
        dispatch_result = {
            "action": "dispatch",
            "task_file": "/home/jay/workspace/memory/tasks/task-381.2.md",
            "team": "dev1-team",
            "level": "normal",
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            events_dir = Path(tmpdir) / "memory" / "events"
            events_dir.mkdir(parents=True, exist_ok=True)

            with (
                patch.object(_mod, "WORKSPACE_ROOT", tmpdir),
                patch.object(_mod, "check_chain_status", return_value=chain_info),
                patch.object(_mod, "dispatch_next_phase", return_value=dispatch_result),
                patch.object(_mod, "send_telegram_notification"),
                patch("subprocess.run") as mock_run,
            ):
                mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

                with patch.dict(
                    os.environ,
                    {"COKACDIR_KEY_ANU": "test-key", "COKACDIR_CHAT_ID": _TEST_CHAT_ID},
                ):
                    with patch("sys.argv", ["notify-completion.py", "task-381.1"]):
                        main()

                dispatch_calls = [c for c in mock_run.call_args_list if any("dispatch.py" in str(arg) for arg in c[0])]
                self.assertTrue(len(dispatch_calls) > 0, "dispatch.py가 subprocess로 호출되지 않았습니다")

    def test_dispatch_failure_sends_telegram_anyway(self) -> None:
        """dispatch.py 호출 실패(returncode=1) → 텔레그램 알림은 여전히 전송됨"""
        chain_info = {
            "in_chain": True,
            "is_last": False,
            "chain_id": "chain-42",
            "next_task_id": "task-381.2",
        }
        dispatch_result = {
            "action": "dispatch",
            "task_file": "/tmp/test-task.md",
            "team": "dev1-team",
            "level": "normal",
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            with (
                patch.object(_mod, "WORKSPACE_ROOT", tmpdir),
                patch.object(_mod, "check_chain_status", return_value=chain_info),
                patch.object(_mod, "dispatch_next_phase", return_value=dispatch_result),
                patch.object(_mod, "send_telegram_notification") as mock_send,
                patch("subprocess.run") as mock_run,
            ):
                mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="dispatch error")

                with patch.dict(os.environ, {"COKACDIR_KEY_ANU": "test-key"}):
                    with patch("sys.argv", ["notify-completion.py", "task-381.1"]):
                        main()

            # dispatch 실패해도 send_telegram_notification은 호출됨
            mock_send.assert_called_once()


if __name__ == "__main__":
    unittest.main(verbosity=2)
