#!/usr/bin/env python3
"""
dispatch.py 및 task-timer.py 재발 방지 로직 테스트

아르고스(테스터)가 작성한 테스트 스위트.
dispatch.py의 팀 가용성 확인, 이중 위임 방지,
task-timer.py의 check-reserved 기능을 검증.
"""

import json
import os
import subprocess
import sys
import tempfile
import importlib
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock, patch, mock_open

import pytest

# sys.path 설정
sys.path.insert(0, "/home/jay/workspace")

# ──────────────────────────────────────────────────────────────
# 공통 픽스처
# ──────────────────────────────────────────────────────────────

@pytest.fixture
def tmp_workspace(tmp_path):
    """임시 워크스페이스 디렉토리 생성"""
    memory_dir = tmp_path / "memory"
    memory_dir.mkdir(parents=True)
    tasks_dir = memory_dir / "tasks"
    tasks_dir.mkdir(parents=True)
    projects_dir = tmp_path / "projects"
    projects_dir.mkdir(parents=True)
    daily_dir = memory_dir / "daily"
    daily_dir.mkdir(parents=True)
    return tmp_path


@pytest.fixture
def timer_file_with_running(tmp_workspace):
    """running 작업이 있는 task-timers.json 생성"""
    timer_data = {
        "tasks": {
            "task-10.1": {
                "task_id": "task-10.1",
                "team_id": "dev1-team",
                "description": "로그인 페이지 개발",
                "status": "running",
                "start_time": datetime.now().isoformat(),
            }
        }
    }
    timer_file = tmp_workspace / "memory" / "task-timers.json"
    timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))
    return timer_file


@pytest.fixture
def timer_file_with_completed(tmp_workspace):
    """completed 작업만 있는 task-timers.json 생성"""
    timer_data = {
        "tasks": {
            "task-9.1": {
                "task_id": "task-9.1",
                "team_id": "dev1-team",
                "description": "이전 작업 완료",
                "status": "completed",
                "start_time": datetime.now().isoformat(),
                "end_time": datetime.now().isoformat(),
            }
        }
    }
    timer_file = tmp_workspace / "memory" / "task-timers.json"
    timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))
    return timer_file


def make_dispatch_func(tmp_workspace, mock_subprocess_result=None):
    """dispatch() 함수를 임시 워크스페이스 환경에서 실행하도록 패치된 버전 반환"""
    if mock_subprocess_result is None:
        mock_subprocess_result = MagicMock(
            returncode=0,
            stdout=json.dumps({"id": "cron-test-123"}),
            stderr="",
        )

    def patched_dispatch(team_id, task_desc, level="normal", session_id=None,
                         project_id=None, force=False, verify=False):
        import dispatch as dispatch_module

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()

        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-anu-key",
                "dev1": "test-dev1-key",
                "dev2": "test-dev2-key",
                "dev3": "test-dev3-key",
            }

            with patch("dispatch.subprocess.run", return_value=mock_subprocess_result):
                with patch("dispatch.generate_task_id", return_value="task-99.1"):
                    # task-timers.json에 task-99.1 placeholder 등록 (generate_task_id mock 대체)
                    timer_file = tmp_workspace / "memory" / "task-timers.json"
                    if timer_file.exists():
                        td = json.loads(timer_file.read_text())
                    else:
                        td = {"tasks": {}}
                    td["tasks"]["task-99.1"] = {
                        "status": "reserved",
                        "reserved_at": datetime.now().isoformat()
                    }
                    timer_file.write_text(json.dumps(td, ensure_ascii=False, indent=2))

                    return dispatch_module.dispatch(
                        team_id, task_desc, level,
                        session_id=session_id,
                        project_id=project_id,
                        force=force,
                        verify=verify,
                    )
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

    return patched_dispatch


# ──────────────────────────────────────────────────────────────
# 시나리오 1: dispatch 전 팀 가용성 확인
# ──────────────────────────────────────────────────────────────

class TestTeamAvailability:
    """dispatch.py: 팀 가용성 확인 테스트"""

    def test_1a_running_task_blocks_dispatch(self, tmp_workspace, timer_file_with_running):
        """1a: team_id에 running 작업이 있을 때 에러 반환 (message에 '--force' 포함)"""
        import dispatch as dispatch_module

        original_workspace = dispatch_module.WORKSPACE
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            result = dispatch_module.dispatch.__wrapped__(
                "dev1-team", "새 작업 설명"
            ) if hasattr(dispatch_module.dispatch, "__wrapped__") else None

            if result is None:
                # 직접 WORKSPACE 패치 후 dispatch 호출
                original_bot_keys = dispatch_module.BOT_KEYS.copy()
                dispatch_module.BOT_KEYS = {
                    "anu": "test-key", "dev1": "test-key",
                    "dev2": "test-key", "dev3": "test-key"
                }
                try:
                    result = dispatch_module.dispatch("dev1-team", "새 작업 설명")
                finally:
                    dispatch_module.BOT_KEYS = original_bot_keys
        finally:
            dispatch_module.WORKSPACE = original_workspace

        assert result["status"] == "error", f"running 작업이 있을 때 error여야 함: {result}"
        assert "--force" in result["message"], f"message에 '--force'가 포함돼야 함: {result['message']}"

    def test_1b_running_task_with_force_proceeds(self, tmp_workspace, timer_file_with_running):
        """1b: running 작업이 있고 force=True → 정상 진행 (mock subprocess)"""
        import dispatch as dispatch_module

        mock_result = MagicMock(
            returncode=0,
            stdout=json.dumps({"id": "cron-abc-123"}),
            stderr="",
        )

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-key", "dev1": "test-key",
                "dev2": "test-key", "dev3": "test-key"
            }
            with patch("dispatch.subprocess.run", return_value=mock_result):
                with patch("dispatch.generate_task_id", return_value="task-99.1"):
                    # placeholder 등록
                    td = json.loads(timer_file_with_running.read_text())
                    td["tasks"]["task-99.1"] = {
                        "status": "reserved",
                        "reserved_at": datetime.now().isoformat()
                    }
                    timer_file_with_running.write_text(
                        json.dumps(td, ensure_ascii=False, indent=2)
                    )
                    result = dispatch_module.dispatch(
                        "dev1-team", "강제 위임 작업", force=True
                    )
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

        assert result["status"] == "dispatched", \
            f"force=True이면 dispatched여야 함: {result}"

    def test_1c_completed_task_allows_dispatch(self, tmp_workspace, timer_file_with_completed):
        """1c: team_id에 completed 작업만 있을 때 정상 진행"""
        import dispatch as dispatch_module

        mock_result = MagicMock(
            returncode=0,
            stdout=json.dumps({"id": "cron-xyz-456"}),
            stderr="",
        )

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-key", "dev1": "test-key",
                "dev2": "test-key", "dev3": "test-key"
            }
            with patch("dispatch.subprocess.run", return_value=mock_result):
                with patch("dispatch.generate_task_id", return_value="task-99.1"):
                    td = json.loads(timer_file_with_completed.read_text())
                    td["tasks"]["task-99.1"] = {
                        "status": "reserved",
                        "reserved_at": datetime.now().isoformat()
                    }
                    timer_file_with_completed.write_text(
                        json.dumps(td, ensure_ascii=False, indent=2)
                    )
                    result = dispatch_module.dispatch(
                        "dev1-team", "새 작업 (이전 completed 상태)"
                    )
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

        assert result["status"] == "dispatched", \
            f"completed 작업만 있을 때 dispatched여야 함: {result}"

    def test_1d_no_timer_file_allows_dispatch(self, tmp_workspace):
        """1d: task-timers.json이 없을 때 정상 진행"""
        import dispatch as dispatch_module

        # timer 파일이 없는지 확인
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        assert not timer_file.exists(), "timer 파일이 없어야 함"

        mock_result = MagicMock(
            returncode=0,
            stdout=json.dumps({"id": "cron-new-789"}),
            stderr="",
        )

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-key", "dev1": "test-key",
                "dev2": "test-key", "dev3": "test-key"
            }
            with patch("dispatch.subprocess.run", return_value=mock_result):
                with patch("dispatch.generate_task_id", return_value="task-99.1"):
                    # generate_task_id mock이 파일을 생성하지 않으므로 수동으로 placeholder 생성
                    td = {"tasks": {"task-99.1": {
                        "status": "reserved",
                        "reserved_at": datetime.now().isoformat()
                    }}}
                    timer_file.write_text(json.dumps(td, ensure_ascii=False, indent=2))
                    result = dispatch_module.dispatch(
                        "dev1-team", "timer 파일 없는 상태에서 첫 위임"
                    )
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

        assert result["status"] == "dispatched", \
            f"timer 파일 없을 때도 dispatched여야 함: {result}"


# ──────────────────────────────────────────────────────────────
# 시나리오 2: 이중 위임 방지
# ──────────────────────────────────────────────────────────────

class TestDoubleDispatchPrevention:
    """dispatch.py: 이중 위임 방지 테스트"""

    @pytest.fixture
    def timer_with_running_desc(self, tmp_workspace):
        """running 상태에 특정 description이 있는 timer 파일 생성.

        description 중복 검사(2단계)를 테스트하기 위해:
        - 팀 가용성 검사(1단계): team_id가 'dev1-team'인 running 작업만 차단
        - 중복 description 검사(2단계): 다른 팀(dev2-team) running 작업의 description과 같은지 비교
        따라서 fixture는 dev1-team과 다른 팀(dev2-team)에 running 작업을 두고,
        dev1-team으로 dispatch 시 description 중복만 걸리도록 설정.
        실제로는 같은 팀 running 작업이 아닌 reserved 상태로 description 중복 테스트가 명확함.
        """
        desc = "동일 작업 - 이미 예약된 로그인 페이지 개발 프로젝트입니다 (reserved)"
        timer_data = {
            "tasks": {
                "task-11.1": {
                    "task_id": "task-11.1",
                    "team_id": "dev1-team",
                    "description": desc[:60],
                    # reserved 상태: 팀 가용성 검사(running만 차단)를 우회하고 description 중복 검사에 걸림
                    "status": "reserved",
                    "reserved_at": datetime.now().isoformat(),
                }
            }
        }
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))
        return timer_file, desc

    @pytest.fixture
    def timer_with_reserved_desc(self, tmp_workspace):
        """reserved 상태에 특정 description이 있는 timer 파일 생성"""
        desc = "동일 작업 - 이미 예약된 API 서버 구축 작업입니다 (예약됨)"
        timer_data = {
            "tasks": {
                "task-12.1": {
                    "task_id": "task-12.1",
                    "team_id": "dev1-team",
                    "description": desc[:60],
                    "status": "reserved",
                    "reserved_at": datetime.now().isoformat(),
                }
            }
        }
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))
        return timer_file, desc

    def test_2a_duplicate_description_blocks_dispatch(self, tmp_workspace, timer_with_running_desc):
        """2a: 같은 description(첫 60자)이 running/reserved에 있을 때 에러 반환.

        dispatch 코드 로직:
        1단계 - 팀 가용성 확인: team_id 일치 + status=running 인 작업이 있으면 차단
        2단계 - 이중 위임 방지: status=running/reserved 이고 description[:60] 동일하면 차단

        reserved 상태 작업으로 테스트하여 1단계를 우회하고 2단계(description 중복)만 검사.
        """
        import dispatch as dispatch_module

        timer_file, desc = timer_with_running_desc

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-key", "dev1": "test-key",
                "dev2": "test-key", "dev3": "test-key"
            }
            # 동일한 description으로 다시 위임 시도
            result = dispatch_module.dispatch("dev1-team", desc)
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

        assert result["status"] == "error", \
            f"중복 description이면 error여야 함: {result}"
        assert "이미 진행 중인 동일 작업" in result["message"], \
            f"message에 중복 안내가 포함돼야 함: {result['message']}"

    def test_2a_reserved_duplicate_description_blocks(self, tmp_workspace, timer_with_reserved_desc):
        """2a-reserved: reserved 상태에도 동일 description이면 에러 반환"""
        import dispatch as dispatch_module

        timer_file, desc = timer_with_reserved_desc

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-key", "dev1": "test-key",
                "dev2": "test-key", "dev3": "test-key"
            }
            result = dispatch_module.dispatch("dev1-team", desc)
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

        assert result["status"] == "error", \
            f"reserved 상태 중복 description이면 error여야 함: {result}"

    def test_2b_task_id_in_prompt_blocks_dispatch(self, tmp_workspace):
        """2b: 프롬프트에 기존 task_id가 포함될 때 에러 반환.

        dispatch 코드 로직:
        1단계 - 팀 가용성 확인: team_id 일치 + running 작업 차단
        2단계 - 이중 위임 방지: 프롬프트에 기존 task_id(running/reserved) 문자열 포함 시 차단

        다른 팀(dev1-team)에 running 작업을 두고, dev2-team으로 dispatch하면서
        프롬프트에 task_id 포함 → 1단계 우회, 2b 검사에서 차단.
        """
        import dispatch as dispatch_module

        # dev1-team에 running 상태 task (팀 가용성 검사는 dev2-team 대상이므로 통과)
        existing_task_id = "task-13.1"
        timer_data = {
            "tasks": {
                existing_task_id: {
                    "task_id": existing_task_id,
                    "team_id": "dev1-team",
                    "description": "기존 작업",
                    "status": "running",
                    "start_time": datetime.now().isoformat(),
                }
            }
        }
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-key", "dev1": "test-key",
                "dev2": "test-key", "dev3": "test-key"
            }
            # dev2-team으로 dispatch, 프롬프트에 dev1-team의 task_id 포함
            task_desc_with_id = f"task-13.1에 대한 후속 작업을 진행하세요"
            result = dispatch_module.dispatch("dev2-team", task_desc_with_id)
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

        assert result["status"] == "error", \
            f"task_id 참조 포함 시 error여야 함: {result}"
        assert "이미 진행 중인 동일 작업" in result["message"], \
            f"message에 중복 안내가 포함돼야 함: {result['message']}"

    def test_2c_force_bypasses_duplicate_check(self, tmp_workspace, timer_with_running_desc):
        """2c: force=True로 중복 작업도 강제 진행 가능"""
        import dispatch as dispatch_module

        timer_file, desc = timer_with_running_desc

        mock_result = MagicMock(
            returncode=0,
            stdout=json.dumps({"id": "cron-force-001"}),
            stderr="",
        )

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-key", "dev1": "test-key",
                "dev2": "test-key", "dev3": "test-key"
            }
            with patch("dispatch.subprocess.run", return_value=mock_result):
                with patch("dispatch.generate_task_id", return_value="task-99.1"):
                    td = json.loads(timer_file.read_text())
                    td["tasks"]["task-99.1"] = {
                        "status": "reserved",
                        "reserved_at": datetime.now().isoformat()
                    }
                    timer_file.write_text(json.dumps(td, ensure_ascii=False, indent=2))
                    result = dispatch_module.dispatch(
                        "dev1-team", desc, force=True
                    )
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

        assert result["status"] == "dispatched", \
            f"force=True 중복 작업도 dispatched여야 함: {result}"


# ──────────────────────────────────────────────────────────────
# 시나리오 3: check-reserved (task-timer.py)
# ──────────────────────────────────────────────────────────────

class TestCheckReserved:
    """task-timer.py: check-reserved 기능 테스트"""

    def _make_timer(self, tmp_workspace, timer_data):
        """임시 워크스페이스에 timer 파일 생성 후 TaskTimer 인스턴스 반환"""
        # task-timer.py를 직접 import (memory/ 경로)
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "task_timer",
            "/home/jay/workspace/memory/task-timer.py"
        )
        task_timer_module = importlib.util.load_from_spec = spec
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)

        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        return mod.TaskTimer(workspace_path=str(tmp_workspace))

    def test_3a_stale_reserved_detected(self, tmp_workspace):
        """3a: reserved 상태가 timeout 이상 지속된 작업 감지 → warning + stale_tasks 반환"""
        # 오래된 reserved_at (1시간 전)
        old_time = (datetime.now() - timedelta(hours=1)).isoformat()
        timer_data = {
            "tasks": {
                "task-20.1": {
                    "task_id": "task-20.1",
                    "team_id": "dev1-team",
                    "description": "오래된 예약 작업",
                    "status": "reserved",
                    "reserved_at": old_time,
                }
            }
        }
        timer = self._make_timer(tmp_workspace, timer_data)

        result = timer.check_reserved(timeout_seconds=300)

        assert result["status"] == "warning", \
            f"stale reserved 작업이 있으면 warning이어야 함: {result}"
        assert len(result["stale_tasks"]) > 0, \
            f"stale_tasks에 항목이 있어야 함: {result}"
        assert result["stale_tasks"][0]["task_id"] == "task-20.1"

    def test_3b_fresh_reserved_ok(self, tmp_workspace):
        """3b: 모든 작업이 timeout 미만일 때 ok, stale_tasks=[]"""
        # 방금 생성된 reserved_at
        fresh_time = datetime.now().isoformat()
        timer_data = {
            "tasks": {
                "task-21.1": {
                    "task_id": "task-21.1",
                    "team_id": "dev2-team",
                    "description": "방금 예약된 작업",
                    "status": "reserved",
                    "reserved_at": fresh_time,
                }
            }
        }
        timer = self._make_timer(tmp_workspace, timer_data)

        # timeout=300초 (5분), 방금 생성된 작업은 걸리지 않아야 함
        result = timer.check_reserved(timeout_seconds=300)

        assert result["status"] == "ok", \
            f"신선한 reserved 작업이면 ok여야 함: {result}"
        assert result["stale_tasks"] == [], \
            f"stale_tasks가 빈 배열이어야 함: {result}"

    def test_3c_reserved_without_reserved_at_ignored(self, tmp_workspace):
        """3c: reserved_at 필드가 없는 작업 → 무시 (에러 없음)"""
        timer_data = {
            "tasks": {
                "task-22.1": {
                    "task_id": "task-22.1",
                    "team_id": "dev3-team",
                    "description": "reserved_at 없는 예약 작업",
                    "status": "reserved",
                    # reserved_at 필드 없음
                }
            }
        }
        timer = self._make_timer(tmp_workspace, timer_data)

        # 에러 없이 실행되어야 함
        result = timer.check_reserved(timeout_seconds=0)

        assert "status" in result, "결과에 status 필드가 있어야 함"
        assert result["stale_tasks"] == [], \
            f"reserved_at 없으면 stale_tasks가 빈 배열이어야 함: {result}"

    def test_3d_cli_check_reserved_timeout_zero(self, tmp_workspace):
        """3d: CLI 실행 테스트: python3 task-timer.py check-reserved --timeout 0"""
        # reserved 작업이 있는 timer 파일 세팅
        old_time = (datetime.now() - timedelta(seconds=10)).isoformat()
        timer_data = {
            "tasks": {
                "task-23.1": {
                    "task_id": "task-23.1",
                    "team_id": "dev1-team",
                    "description": "CLI 테스트용 예약 작업",
                    "status": "reserved",
                    "reserved_at": old_time,
                }
            }
        }
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        env = os.environ.copy()
        env["WORKSPACE_ROOT"] = str(tmp_workspace)

        proc = subprocess.run(
            ["python3", "/home/jay/workspace/memory/task-timer.py",
             "check-reserved", "--timeout", "0"],
            capture_output=True, text=True, env=env
        )

        assert proc.returncode == 0, \
            f"CLI 실행이 성공해야 함 (returncode=0): stderr={proc.stderr}"

        output = json.loads(proc.stdout)
        assert output["status"] == "warning", \
            f"timeout=0이면 reserved 작업이 감지돼야 함: {output}"
        assert len(output["stale_tasks"]) > 0, \
            f"stale_tasks에 작업이 있어야 함: {output}"


# ──────────────────────────────────────────────────────────────
# 시나리오 4: cron_id / expected_execution 포함 (dispatch.py)
# ──────────────────────────────────────────────────────────────

class TestDispatchResultFields:
    """dispatch.py: 결과 필드 포함 여부 테스트"""

    @pytest.fixture
    def clean_timer_file(self, tmp_workspace):
        """빈 timer 파일 생성"""
        timer_data = {"tasks": {}}
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))
        return timer_file

    def _dispatch_with_mock(self, tmp_workspace, timer_file, task_desc,
                             verify=False, cron_response=None):
        """mock subprocess로 dispatch 실행"""
        import dispatch as dispatch_module

        if cron_response is None:
            cron_response = {"id": "cron-field-test-001"}

        mock_result = MagicMock(
            returncode=0,
            stdout=json.dumps(cron_response),
            stderr="",
        )

        original_workspace = dispatch_module.WORKSPACE
        original_bot_keys = dispatch_module.BOT_KEYS.copy()
        try:
            dispatch_module.WORKSPACE = tmp_workspace
            dispatch_module.BOT_KEYS = {
                "anu": "test-key", "dev1": "test-key",
                "dev2": "test-key", "dev3": "test-key"
            }
            with patch("dispatch.subprocess.run", return_value=mock_result):
                with patch("dispatch.generate_task_id", return_value="task-99.1"):
                    td = json.loads(timer_file.read_text())
                    td["tasks"]["task-99.1"] = {
                        "status": "reserved",
                        "reserved_at": datetime.now().isoformat()
                    }
                    timer_file.write_text(json.dumps(td, ensure_ascii=False, indent=2))
                    return dispatch_module.dispatch(
                        "dev1-team", task_desc, verify=verify
                    )
        finally:
            dispatch_module.WORKSPACE = original_workspace
            dispatch_module.BOT_KEYS = original_bot_keys

    def test_4a_cron_id_in_result(self, tmp_workspace, clean_timer_file):
        """dispatch 성공 결과에 cron_id 필드가 포함되는지 확인"""
        result = self._dispatch_with_mock(
            tmp_workspace, clean_timer_file,
            "cron_id 테스트 작업",
            cron_response={"id": "cron-12345"}
        )

        assert result["status"] == "dispatched", f"dispatched여야 함: {result}"
        assert "cron_id" in result, f"cron_id 필드가 있어야 함: {result}"
        assert result["cron_id"] == "cron-12345", \
            f"cron_id가 'cron-12345'여야 함: {result}"

    def test_4b_expected_execution_in_result(self, tmp_workspace, clean_timer_file):
        """dispatch 성공 결과에 expected_execution 필드가 포함되는지 확인"""
        result = self._dispatch_with_mock(
            tmp_workspace, clean_timer_file,
            "expected_execution 테스트 작업"
        )

        assert result["status"] == "dispatched", f"dispatched여야 함: {result}"
        assert "expected_execution" in result, \
            f"expected_execution 필드가 있어야 함: {result}"
        # 시간 포맷 확인 (YYYY-MM-DD HH:MM:SS)
        exec_time = result["expected_execution"]
        assert len(exec_time) == 19, \
            f"expected_execution 포맷이 올바르지 않음: {exec_time}"

    def test_4c_verify_true_includes_verify_note(self, tmp_workspace, clean_timer_file):
        """verify=True일 때 verify_note 필드가 포함되는지 확인"""
        result = self._dispatch_with_mock(
            tmp_workspace, clean_timer_file,
            "verify 테스트 작업",
            verify=True
        )

        assert result["status"] == "dispatched", f"dispatched여야 함: {result}"
        assert "verify_note" in result, \
            f"verify=True일 때 verify_note 필드가 있어야 함: {result}"
        assert len(result["verify_note"]) > 0, \
            f"verify_note가 비어있으면 안 됨: {result}"

    def test_4d_verify_false_no_verify_note(self, tmp_workspace, clean_timer_file):
        """verify=False(기본값)일 때 verify_note 필드가 없는지 확인"""
        result = self._dispatch_with_mock(
            tmp_workspace, clean_timer_file,
            "verify False 테스트 작업",
            verify=False
        )

        assert result["status"] == "dispatched", f"dispatched여야 함: {result}"
        assert "verify_note" not in result, \
            f"verify=False일 때 verify_note가 없어야 함: {result}"

    def test_4e_cron_id_none_when_no_id_in_response(self, tmp_workspace, clean_timer_file):
        """cokacdir 응답에 id 필드가 없을 때 cron_id가 None인지 확인"""
        result = self._dispatch_with_mock(
            tmp_workspace, clean_timer_file,
            "id 없는 응답 테스트",
            cron_response={"status": "ok"}  # id 필드 없음
        )

        assert result["status"] == "dispatched", f"dispatched여야 함: {result}"
        assert "cron_id" in result, f"cron_id 필드는 있어야 함: {result}"
        assert result["cron_id"] is None, \
            f"id 없는 응답이면 cron_id가 None이어야 함: {result}"


# ──────────────────────────────────────────────────────────────
# 추가: TaskTimer 직접 import 테스트
# ──────────────────────────────────────────────────────────────

class TestTaskTimerImport:
    """task-timer.py TaskTimer 클래스 직접 import 테스트"""

    def _load_task_timer_module(self):
        """task-timer.py 모듈 로드"""
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "task_timer_module",
            "/home/jay/workspace/memory/task-timer.py"
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        return mod

    def test_task_timer_check_reserved_stale(self, tmp_workspace):
        """TaskTimer.check_reserved: 오래된 reserved 작업 감지"""
        mod = self._load_task_timer_module()

        old_time = (datetime.now() - timedelta(minutes=10)).isoformat()
        timer_data = {
            "tasks": {
                "task-30.1": {
                    "status": "reserved",
                    "reserved_at": old_time,
                }
            }
        }
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        timer = mod.TaskTimer(workspace_path=str(tmp_workspace))
        result = timer.check_reserved(timeout_seconds=60)

        assert result["status"] == "warning"
        stale = result["stale_tasks"]
        assert len(stale) == 1
        assert stale[0]["task_id"] == "task-30.1"
        assert stale[0]["age_seconds"] >= 60

    def test_task_timer_check_reserved_ok(self, tmp_workspace):
        """TaskTimer.check_reserved: 신선한 reserved 작업이면 ok"""
        mod = self._load_task_timer_module()

        fresh_time = datetime.now().isoformat()
        timer_data = {
            "tasks": {
                "task-31.1": {
                    "status": "reserved",
                    "reserved_at": fresh_time,
                }
            }
        }
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        timer = mod.TaskTimer(workspace_path=str(tmp_workspace))
        result = timer.check_reserved(timeout_seconds=300)

        assert result["status"] == "ok"
        assert result["stale_tasks"] == []

    def test_task_timer_check_reserved_no_reserved_at(self, tmp_workspace):
        """TaskTimer.check_reserved: reserved_at 없는 작업 무시"""
        mod = self._load_task_timer_module()

        timer_data = {
            "tasks": {
                "task-32.1": {
                    "status": "reserved",
                    # reserved_at 없음
                }
            }
        }
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        timer = mod.TaskTimer(workspace_path=str(tmp_workspace))
        # 에러 없이 실행되어야 함
        result = timer.check_reserved(timeout_seconds=0)

        assert result["status"] == "ok"
        assert result["stale_tasks"] == []

    def test_task_timer_check_reserved_mixed(self, tmp_workspace):
        """TaskTimer.check_reserved: stale와 fresh가 섞인 경우 stale만 반환"""
        mod = self._load_task_timer_module()

        old_time = (datetime.now() - timedelta(hours=2)).isoformat()
        fresh_time = datetime.now().isoformat()
        timer_data = {
            "tasks": {
                "task-33.1": {
                    "status": "reserved",
                    "reserved_at": old_time,
                },
                "task-33.2": {
                    "status": "reserved",
                    "reserved_at": fresh_time,
                },
                "task-33.3": {
                    "status": "running",
                    "start_time": old_time,  # running은 무시되어야 함
                },
            }
        }
        timer_file = tmp_workspace / "memory" / "task-timers.json"
        timer_file.write_text(json.dumps(timer_data, ensure_ascii=False, indent=2))

        timer = mod.TaskTimer(workspace_path=str(tmp_workspace))
        result = timer.check_reserved(timeout_seconds=300)

        assert result["status"] == "warning"
        assert len(result["stale_tasks"]) == 1
        assert result["stale_tasks"][0]["task_id"] == "task-33.1"
