"""
dispatch.py 수정사항 테스트
테스터: 아르고스

테스트 대상 변경 사항:
1. dispatch()에 task-timer 자동 시작 추가 (generate_task_id() 직후 subprocess 호출)
2. _cleanup_reserved() → _cleanup_task()로 이름 변경 및 확장 (reserved + running 정리)
3. dispatch 실패 시 _cleanup_task(task_id) 호출
"""

import json
import sys
import tempfile
import unittest
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, call, patch

# dispatch 모듈 경로 추가
sys.path.insert(0, "/home/jay/workspace")


class TestDispatchAutoTimer(unittest.TestCase):
    """dispatch() 실행 시 task-timer 자동 시작 테스트"""

    def setUp(self):
        """각 테스트 전 임시 디렉토리 및 공통 mock 설정"""
        self.tmp_dir = tempfile.TemporaryDirectory()
        self.tmp_path = Path(self.tmp_dir.name)

        # 필요한 디렉토리 생성
        (self.tmp_path / "memory").mkdir(parents=True)
        (self.tmp_path / "memory" / "tasks").mkdir(parents=True)

        # task-timers.json 초기화 (빈 상태)
        self.timer_file = self.tmp_path / "memory" / "task-timers.json"
        self.timer_file.write_text(json.dumps({"tasks": {}}), encoding="utf-8")

        # task-timer.py 경로
        self.task_timer_path = self.tmp_path / "memory" / "task-timer.py"

    def tearDown(self):
        self.tmp_dir.cleanup()

    def _make_subprocess_success(self, task_id="task-1.1"):
        """성공하는 subprocess.run mock 반환 (cokacdir 응답 포함)"""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = json.dumps({"session_id": "sess-001"})
        mock_result.stderr = ""
        return mock_result

    def _make_subprocess_fail(self):
        """실패하는 subprocess.run mock 반환"""
        mock_result = MagicMock()
        mock_result.returncode = 1
        mock_result.stdout = ""
        mock_result.stderr = "cokacdir 연결 실패"
        return mock_result

    def _patch_dispatch_env(self, subprocess_side_effects, task_id="task-1.1"):
        """dispatch() 실행을 위한 공통 패치 컨텍스트 반환"""
        patches = [
            patch("dispatch.WORKSPACE", self.tmp_path),
            patch("dispatch.TASK_TIMER", self.task_timer_path),
            patch("dispatch.BOT_KEYS", {"dev1": "test-key-dev1", "anu": "test-key-anu"}),
            patch("dispatch.CHAT_ID", "1234567890"),
            patch("dispatch.generate_task_id", return_value=task_id),
            patch("dispatch.build_prompt", return_value="테스트 프롬프트"),
            patch("dispatch.subprocess.run", side_effect=subprocess_side_effects),
        ]
        return patches

    def test_task_running_status_in_timer_file_after_dispatch(self):
        """
        테스트 1: dispatch() 실행 후 task-timers.json에서 해당 task가 running 상태인지 확인.

        task-timer start 호출 시 task-timers.json에 status=running으로 기록되어야 한다.
        실제 task-timer.py 실행 없이, subprocess.run이 호출되면 수동으로 timer_file을 업데이트.
        """
        task_id = "task-1.1"

        # task-timer start가 호출되면 timer_file에 running 상태로 기록하는 side_effect
        def subprocess_side_effect(cmd, **kwargs):
            if "start" in cmd and str(self.task_timer_path) in " ".join(str(c) for c in cmd):
                # task-timer start 호출: timer_file을 running 상태로 업데이트
                timer_data = json.loads(self.timer_file.read_text())
                timer_data["tasks"][task_id] = {
                    "task_id": task_id,
                    "team_id": "dev1-team",
                    "description": "테스트 작업",
                    "status": "running",
                    "start_time": datetime.now().isoformat(),
                }
                self.timer_file.write_text(json.dumps(timer_data), encoding="utf-8")
                mock = MagicMock()
                mock.returncode = 0
                mock.stdout = json.dumps({"status": "started"})
                mock.stderr = ""
                return mock
            else:
                # cokacdir --cron 또는 log 호출
                mock = MagicMock()
                mock.returncode = 0
                mock.stdout = json.dumps({"session_id": "sess-001"})
                mock.stderr = ""
                return mock

        import dispatch

        with (
            patch("dispatch.WORKSPACE", self.tmp_path),
            patch("dispatch.TASK_TIMER", self.task_timer_path),
            patch("dispatch.BOT_KEYS", {"dev1": "test-key-dev1", "anu": "test-key-anu"}),
            patch("dispatch.CHAT_ID", "1234567890"),
            patch("dispatch.generate_task_id", return_value=task_id),
            patch("dispatch.build_prompt", return_value="테스트 프롬프트"),
            patch("dispatch.subprocess.run", side_effect=subprocess_side_effect),
        ):

            result = dispatch.dispatch("dev1-team", "테스트 작업")

        # dispatch 성공 확인
        self.assertEqual(result["status"], "dispatched")
        self.assertEqual(result["task_id"], task_id)

        # timer_file에 running 상태로 기록되었는지 확인
        timer_data = json.loads(self.timer_file.read_text())
        task_entry = timer_data["tasks"].get(task_id)
        self.assertIsNotNone(task_entry, f"{task_id}가 task-timers.json에 존재해야 함")
        self.assertEqual(
            task_entry["status"],
            "running",
            f"dispatch 후 {task_id}의 상태는 'running'이어야 함, 실제: {task_entry['status']}",
        )

    def test_task_timer_start_called_with_correct_args(self):
        """
        테스트 2: task-timer.py start가 올바른 인자로 호출되는지 확인.

        dispatch()는 generate_task_id() 직후 다음 명령을 실행해야 한다:
        python3 <TASK_TIMER> start <task_id> --team <team_id> --desc <short_desc>
        """
        task_id = "task-2.1"
        task_desc = "테스트 작업 설명입니다"

        call_args_list = []

        def capturing_subprocess(cmd, **kwargs):
            call_args_list.append(list(str(c) for c in cmd))
            mock = MagicMock()
            mock.returncode = 0
            mock.stdout = json.dumps({"session_id": "sess-002"})
            mock.stderr = ""
            return mock

        import dispatch

        with (
            patch("dispatch.WORKSPACE", self.tmp_path),
            patch("dispatch.TASK_TIMER", self.task_timer_path),
            patch("dispatch.BOT_KEYS", {"dev1": "test-key-dev1", "anu": "test-key-anu"}),
            patch("dispatch.CHAT_ID", "1234567890"),
            patch("dispatch.generate_task_id", return_value=task_id),
            patch("dispatch.build_prompt", return_value="테스트 프롬프트"),
            patch("dispatch.subprocess.run", side_effect=capturing_subprocess),
        ):

            dispatch.dispatch("dev1-team", task_desc)

        # subprocess.run 호출 중 task-timer start가 포함되어야 함
        timer_calls = [c for c in call_args_list if "start" in c and str(self.task_timer_path) in c]
        self.assertGreater(len(timer_calls), 0, "task-timer start 호출이 없음")

        timer_call = timer_calls[0]
        # python3 <path> start <task_id> --team <team_id> --desc <desc> 형식 확인
        self.assertIn("python3", timer_call)
        self.assertIn(str(self.task_timer_path), timer_call)
        self.assertIn("start", timer_call)
        self.assertIn(task_id, timer_call)
        self.assertIn("--team", timer_call)
        self.assertIn("dev1-team", timer_call)
        self.assertIn("--desc", timer_call)

        # --desc 다음 값이 task_desc의 앞 60자인지 확인
        desc_idx = timer_call.index("--desc")
        actual_desc = timer_call[desc_idx + 1]
        expected_desc = task_desc[:60] + ("..." if len(task_desc) > 60 else "")
        self.assertEqual(actual_desc, expected_desc, f"--desc 값이 올바르지 않음: {actual_desc!r} != {expected_desc!r}")

    def test_task_timer_start_called_before_cokacdir(self):
        """
        테스트 2 추가: task-timer start가 cokacdir --cron보다 먼저 호출되는지 순서 확인.

        코드 구조상 timer_cmd 실행이 cokacdir cmd 실행보다 먼저여야 한다.
        """
        task_id = "task-3.1"
        call_order = []

        def ordered_subprocess(cmd, **kwargs):
            cmd_str = " ".join(str(c) for c in cmd)
            if str(self.task_timer_path) in cmd_str and "start" in cmd_str:
                call_order.append("timer_start")
            elif "cokacdir" in cmd_str and "--cron" in cmd_str and "log" not in cmd_str:
                call_order.append("cokacdir_cron")
            elif str(self.task_timer_path) in cmd_str and "log" in cmd_str:
                call_order.append("timer_log")
            mock = MagicMock()
            mock.returncode = 0
            mock.stdout = json.dumps({"session_id": "sess-003"})
            mock.stderr = ""
            return mock

        import dispatch

        with (
            patch("dispatch.WORKSPACE", self.tmp_path),
            patch("dispatch.TASK_TIMER", self.task_timer_path),
            patch("dispatch.BOT_KEYS", {"dev1": "test-key-dev1", "anu": "test-key-anu"}),
            patch("dispatch.CHAT_ID", "1234567890"),
            patch("dispatch.generate_task_id", return_value=task_id),
            patch("dispatch.build_prompt", return_value="테스트 프롬프트"),
            patch("dispatch.subprocess.run", side_effect=ordered_subprocess),
        ):

            dispatch.dispatch("dev1-team", "순서 확인 테스트")

        self.assertIn("timer_start", call_order, "task-timer start 호출이 없음")
        self.assertIn("cokacdir_cron", call_order, "cokacdir --cron 호출이 없음")

        timer_idx = call_order.index("timer_start")
        cokacdir_idx = call_order.index("cokacdir_cron")
        self.assertLess(
            timer_idx,
            cokacdir_idx,
            f"task-timer start({timer_idx})가 cokacdir --cron({cokacdir_idx})보다 먼저 호출되어야 함",
        )


class TestCleanupTask(unittest.TestCase):
    """_cleanup_task() 함수 동작 테스트"""

    def setUp(self):
        self.tmp_dir = tempfile.TemporaryDirectory()
        self.tmp_path = Path(self.tmp_dir.name)
        (self.tmp_path / "memory").mkdir(parents=True)
        self.timer_file = self.tmp_path / "memory" / "task-timers.json"

    def tearDown(self):
        self.tmp_dir.cleanup()

    def _write_timer_file(self, tasks: dict):
        """task-timers.json에 지정된 tasks 데이터 작성"""
        self.timer_file.write_text(json.dumps({"tasks": tasks}, ensure_ascii=False, indent=2), encoding="utf-8")

    def _read_timer_file(self) -> dict:
        """task-timers.json 읽어서 tasks 딕셔너리 반환"""
        data = json.loads(self.timer_file.read_text(encoding="utf-8"))
        return data.get("tasks", {})

    def test_cleanup_task_deletes_reserved_entry(self):
        """
        테스트 3: _cleanup_task()가 reserved 상태 엔트리를 삭제하는지 확인.

        task-timers.json에 reserved 상태의 task가 있을 때
        _cleanup_task()를 호출하면 해당 task가 삭제되어야 한다.
        """
        task_id = "task-10.1"
        self._write_timer_file({task_id: {"status": "reserved", "reserved_at": datetime.now().isoformat()}})

        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            dispatch._cleanup_task(task_id)

        tasks = self._read_timer_file()
        self.assertNotIn(task_id, tasks, f"reserved 상태의 {task_id}가 _cleanup_task() 후 삭제되어야 함")

    def test_cleanup_task_deletes_running_entry(self):
        """
        테스트 4: _cleanup_task()가 running 상태 엔트리를 삭제하는지 확인.

        task-timer 자동 시작 도입으로 dispatch 실패 시 이미 running 상태일 수 있음.
        _cleanup_task()는 running 상태도 삭제 대상이어야 한다.
        """
        task_id = "task-11.1"
        self._write_timer_file(
            {
                task_id: {
                    "task_id": task_id,
                    "team_id": "dev1-team",
                    "description": "실패한 작업",
                    "status": "running",
                    "start_time": datetime.now().isoformat(),
                }
            }
        )

        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            dispatch._cleanup_task(task_id)

        tasks = self._read_timer_file()
        self.assertNotIn(task_id, tasks, f"running 상태의 {task_id}가 _cleanup_task() 후 삭제되어야 함")

    def test_cleanup_task_skips_completed_entry(self):
        """
        테스트 5: _cleanup_task()가 completed 상태는 건너뛰는지 확인.

        completed 상태는 정상 종료된 작업이므로 삭제하면 안 된다.
        _cleanup_task() 호출 후에도 completed 엔트리가 그대로 남아있어야 한다.
        """
        task_id = "task-12.1"
        completed_entry = {
            "task_id": task_id,
            "team_id": "dev1-team",
            "description": "완료된 작업",
            "status": "completed",
            "start_time": "2026-03-04T10:00:00",
            "end_time": "2026-03-04T11:00:00",
            "duration_seconds": 3600,
        }
        self._write_timer_file({task_id: completed_entry})

        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            dispatch._cleanup_task(task_id)

        tasks = self._read_timer_file()
        self.assertIn(task_id, tasks, f"completed 상태의 {task_id}는 _cleanup_task() 후에도 남아있어야 함")
        self.assertEqual(tasks[task_id]["status"], "completed", "completed 엔트리의 상태가 변경되어서는 안 됨")

    def test_cleanup_task_called_on_dispatch_failure(self):
        """
        테스트 3+: cokacdir --cron 실패 시 _cleanup_task()가 호출되는지 확인.

        dispatch()에서 cokacdir 호출이 실패(returncode != 0)하면
        _cleanup_task(task_id)를 호출해야 한다.
        """
        task_id = "task-20.1"

        # timer_file 초기화 (reserved 상태)
        (self.tmp_path / "memory" / "tasks").mkdir(parents=True)
        self.timer_file.write_text(
            json.dumps({"tasks": {task_id: {"status": "reserved", "reserved_at": datetime.now().isoformat()}}}),
            encoding="utf-8",
        )
        task_timer_path = self.tmp_path / "memory" / "task-timer.py"

        def subprocess_side_effect(cmd, **kwargs):
            cmd_str = " ".join(str(c) for c in cmd)
            mock = MagicMock()
            if "cokacdir" in cmd_str and "--cron" in cmd_str:
                # cokacdir 호출 실패
                mock.returncode = 1
                mock.stdout = ""
                mock.stderr = "연결 실패"
            else:
                mock.returncode = 0
                mock.stdout = json.dumps({"status": "ok"})
                mock.stderr = ""
            return mock

        import dispatch

        with (
            patch("dispatch.WORKSPACE", self.tmp_path),
            patch("dispatch.TASK_TIMER", task_timer_path),
            patch("dispatch.BOT_KEYS", {"dev1": "test-key-dev1", "anu": "test-key-anu"}),
            patch("dispatch.CHAT_ID", "1234567890"),
            patch("dispatch.generate_task_id", return_value=task_id),
            patch("dispatch.build_prompt", return_value="테스트 프롬프트"),
            patch("dispatch._cleanup_task") as mock_cleanup,
            patch("dispatch.subprocess.run", side_effect=subprocess_side_effect),
        ):

            result = dispatch.dispatch("dev1-team", "실패할 작업")

        # dispatch가 error 상태 반환
        self.assertEqual(result["status"], "error")

        # _cleanup_task(task_id)가 호출되었는지 확인
        mock_cleanup.assert_called_once_with(task_id)

    def test_cleanup_task_only_deletes_target_task(self):
        """
        _cleanup_task()가 지정된 task만 삭제하고 다른 task는 보존하는지 확인.
        """
        target_id = "task-30.1"
        other_id = "task-30.2"

        self._write_timer_file(
            {
                target_id: {"status": "reserved", "reserved_at": datetime.now().isoformat()},
                other_id: {
                    "task_id": other_id,
                    "status": "running",
                    "start_time": datetime.now().isoformat(),
                },
            }
        )

        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            dispatch._cleanup_task(target_id)

        tasks = self._read_timer_file()
        self.assertNotIn(target_id, tasks, f"{target_id}가 삭제되어야 함")
        self.assertIn(other_id, tasks, f"{other_id}는 삭제되지 않아야 함")

    def test_cleanup_task_nonexistent_task_no_error(self):
        """
        _cleanup_task()에 존재하지 않는 task_id를 전달해도 예외 없이 정상 종료되어야 한다.
        """
        self._write_timer_file({})

        import dispatch

        # 예외가 발생하지 않아야 함
        try:
            with patch("dispatch.WORKSPACE", self.tmp_path):
                dispatch._cleanup_task("task-nonexistent-999.1")
        except Exception as e:
            self.fail(f"_cleanup_task()가 예외를 발생시켰음: {e}")


class TestCleanupTaskStatusCoverage(unittest.TestCase):
    """_cleanup_task() 상태별 동작 종합 확인"""

    def setUp(self):
        self.tmp_dir = tempfile.TemporaryDirectory()
        self.tmp_path = Path(self.tmp_dir.name)
        (self.tmp_path / "memory").mkdir(parents=True)
        self.timer_file = self.tmp_path / "memory" / "task-timers.json"

    def tearDown(self):
        self.tmp_dir.cleanup()

    def _setup_task(self, task_id: str, status: str) -> dict:
        """지정 상태의 task를 timer_file에 기록하고 task 데이터 반환"""
        task_entry = {"task_id": task_id, "status": status}
        if status == "reserved":
            task_entry["reserved_at"] = datetime.now().isoformat()
        elif status in ("running", "stale"):
            task_entry["start_time"] = datetime.now().isoformat()
        elif status == "completed":
            task_entry["start_time"] = "2026-03-04T09:00:00"
            task_entry["end_time"] = "2026-03-04T10:00:00"

        self.timer_file.write_text(
            json.dumps({"tasks": {task_id: task_entry}}, ensure_ascii=False, indent=2), encoding="utf-8"
        )
        return task_entry

    def _task_exists(self, task_id: str) -> bool:
        data = json.loads(self.timer_file.read_text(encoding="utf-8"))
        return task_id in data.get("tasks", {})

    def test_reserved_is_deleted(self):
        """reserved 상태 → 삭제"""
        task_id = "task-100.1"
        self._setup_task(task_id, "reserved")
        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            dispatch._cleanup_task(task_id)
        self.assertFalse(self._task_exists(task_id), "reserved 상태는 삭제되어야 함")

    def test_running_is_deleted(self):
        """running 상태 → 삭제"""
        task_id = "task-101.1"
        self._setup_task(task_id, "running")
        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            dispatch._cleanup_task(task_id)
        self.assertFalse(self._task_exists(task_id), "running 상태는 삭제되어야 함")

    def test_completed_is_preserved(self):
        """completed 상태 → 보존"""
        task_id = "task-102.1"
        self._setup_task(task_id, "completed")
        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            dispatch._cleanup_task(task_id)
        self.assertTrue(self._task_exists(task_id), "completed 상태는 보존되어야 함")

    def test_stale_is_preserved(self):
        """stale 상태 → 보존 (정리 대상 아님)"""
        task_id = "task-103.1"
        self._setup_task(task_id, "stale")
        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            dispatch._cleanup_task(task_id)
        self.assertTrue(self._task_exists(task_id), "stale 상태는 보존되어야 함")


class TestCleanupUsesTimerTaskId(unittest.TestCase):
    """_cleanup_task가 timer_task_id (= task_id)로 호출되는지 확인"""

    def setUp(self):
        self.tmp_dir = tempfile.TemporaryDirectory()
        self.tmp_path = Path(self.tmp_dir.name)
        (self.tmp_path / "memory").mkdir(parents=True)
        (self.tmp_path / "memory" / "tasks").mkdir(parents=True)
        self.timer_file = self.tmp_path / "memory" / "task-timers.json"
        self.timer_file.write_text(json.dumps({"tasks": {}}), encoding="utf-8")
        self.task_timer_path = self.tmp_path / "memory" / "task-timer.py"

    def tearDown(self):
        self.tmp_dir.cleanup()

    def test_cleanup_uses_timer_task_id_not_raw_task_id(self):
        """task_id에 점이 없는 경우(task-20), _cleanup_task가 task-20으로 호출되어야 함"""
        task_id = "task-20"  # 점 없음
        expected_timer_task_id = "task-20"

        def subprocess_side_effect(cmd, **kwargs):
            cmd_str = " ".join(str(c) for c in cmd)
            mock = MagicMock()
            if "cokacdir" in cmd_str and "--cron" in cmd_str:
                mock.returncode = 1
                mock.stdout = ""
                mock.stderr = "연결 실패"
            else:
                mock.returncode = 0
                mock.stdout = json.dumps({"status": "ok"})
                mock.stderr = ""
            return mock

        import dispatch

        with (
            patch("dispatch.WORKSPACE", self.tmp_path),
            patch("dispatch.TASK_TIMER", self.task_timer_path),
            patch("dispatch.BOT_KEYS", {"dev1": "test-key-dev1", "anu": "test-key-anu"}),
            patch("dispatch.CHAT_ID", "1234567890"),
            patch("dispatch.generate_task_id", return_value=task_id),
            patch("dispatch.build_prompt", return_value="테스트 프롬프트"),
            patch("dispatch._cleanup_task") as mock_cleanup,
            patch("dispatch.subprocess.run", side_effect=subprocess_side_effect),
        ):

            result = dispatch.dispatch("dev1-team", "실패 테스트")

        self.assertEqual(result["status"], "error")
        mock_cleanup.assert_called_once_with(expected_timer_task_id)

    def test_cleanup_with_dotted_task_id_same(self):
        """task_id에 점이 있는 경우(task-20.1), _cleanup_task도 task-20.1로 호출"""
        task_id = "task-20.1"

        def subprocess_side_effect(cmd, **kwargs):
            cmd_str = " ".join(str(c) for c in cmd)
            mock = MagicMock()
            if "cokacdir" in cmd_str and "--cron" in cmd_str:
                mock.returncode = 1
                mock.stdout = ""
                mock.stderr = "연결 실패"
            else:
                mock.returncode = 0
                mock.stdout = json.dumps({"status": "ok"})
                mock.stderr = ""
            return mock

        import dispatch

        with (
            patch("dispatch.WORKSPACE", self.tmp_path),
            patch("dispatch.TASK_TIMER", self.task_timer_path),
            patch("dispatch.BOT_KEYS", {"dev1": "test-key-dev1", "anu": "test-key-anu"}),
            patch("dispatch.CHAT_ID", "1234567890"),
            patch("dispatch.generate_task_id", return_value=task_id),
            patch("dispatch.build_prompt", return_value="테스트 프롬프트"),
            patch("dispatch._cleanup_task") as mock_cleanup,
            patch("dispatch.subprocess.run", side_effect=subprocess_side_effect),
        ):

            result = dispatch.dispatch("dev1-team", "실패 테스트")

        self.assertEqual(result["status"], "error")
        mock_cleanup.assert_called_once_with(task_id)


class TestCleanupOnBotIdMissing(unittest.TestCase):
    """bot_id 미할당 시에도 _cleanup_task 호출 확인 (Fix 2)"""

    def setUp(self):
        self.tmp_dir = tempfile.TemporaryDirectory()
        self.tmp_path = Path(self.tmp_dir.name)
        (self.tmp_path / "memory").mkdir(parents=True)
        (self.tmp_path / "memory" / "tasks").mkdir(parents=True)
        self.timer_file = self.tmp_path / "memory" / "task-timers.json"
        self.timer_file.write_text(json.dumps({"tasks": {}}), encoding="utf-8")
        self.task_timer_path = self.tmp_path / "memory" / "task-timer.py"

    def tearDown(self):
        self.tmp_dir.cleanup()

    def test_cleanup_called_when_bot_id_not_in_bot_keys(self):
        """TEAM_BOT에 매핑이 있지만 BOT_KEYS에 없는 경우 cleanup 호출 확인"""
        task_id = "task-50.1"

        def subprocess_side_effect(cmd, **kwargs):
            mock = MagicMock()
            mock.returncode = 0
            mock.stdout = json.dumps({"status": "ok"})
            mock.stderr = ""
            return mock

        import dispatch

        with (
            patch("dispatch.WORKSPACE", self.tmp_path),
            patch("dispatch.TASK_TIMER", self.task_timer_path),
            patch("dispatch.BOT_KEYS", {"anu": "test-key-anu"}),
            patch("dispatch.TEAM_BOT", {"dev1-team": "dev1"}),
            patch("dispatch.CHAT_ID", "1234567890"),
            patch("dispatch.generate_task_id", return_value=task_id),
            patch("dispatch.build_prompt", return_value="테스트 프롬프트"),
            patch("dispatch._cleanup_task") as mock_cleanup,
            patch("dispatch.subprocess.run", side_effect=subprocess_side_effect),
        ):

            result = dispatch.dispatch("dev1-team", "bot_id 없는 테스트")

        self.assertEqual(result["status"], "error")
        self.assertIn("봇이 없습니다", result["message"])
        mock_cleanup.assert_called_once_with(task_id)


class TestGenerateTaskIdCorrupted(unittest.TestCase):
    """generate_task_id() - 파일 손상 시 RuntimeError 발생 확인 (Fix 6)"""

    def setUp(self):
        self.tmp_dir = tempfile.TemporaryDirectory()
        self.tmp_path = Path(self.tmp_dir.name)
        (self.tmp_path / "memory").mkdir(parents=True)
        self.timer_file = self.tmp_path / "memory" / "task-timers.json"

    def tearDown(self):
        self.tmp_dir.cleanup()

    def test_corrupted_file_raises_runtime_error(self):
        """파일이 존재하지만 손상된 경우 RuntimeError 발생"""
        self.timer_file.write_text("NOT VALID JSON {{{", encoding="utf-8")

        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            with self.assertRaises(RuntimeError) as ctx:
                dispatch.generate_task_id()
            self.assertIn("손상", str(ctx.exception))

    def test_nonexistent_file_returns_task_1_1(self):
        """파일이 없으면 task-1.1 반환 (정상 동작)"""
        if self.timer_file.exists():
            self.timer_file.unlink()

        import dispatch

        with patch("dispatch.WORKSPACE", self.tmp_path):
            result = dispatch.generate_task_id()
        self.assertEqual(result, "task-1.1")


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