"""
test_dispatch_resume.py

dispatch.py --resume-from 옵션 단위 테스트 (스바로그 작성)

테스트 항목:
- resume_from=None이면 task_desc 변경 없음
- resume_from이 존재하는 파일 → task_desc 앞에 요약 prepend
- resume_from이 존재하지 않는 파일 → error 반환
- CLI argparse가 --resume-from 옵션을 정상 파싱
"""

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

import pytest

_WORKSPACE = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
if str(_WORKSPACE) not in sys.path:
    sys.path.insert(0, str(_WORKSPACE))


# ---------------------------------------------------------------------------
# 헬퍼: dispatch 모듈을 격리된 WORKSPACE로 재로드
# ---------------------------------------------------------------------------


def _load_dispatch_with_workspace(tmp_path: Path) -> types.ModuleType:
    """dispatch 모듈을 tmp_path를 WORKSPACE로 설정하여 로드한다."""
    workspace = _WORKSPACE
    if str(workspace) not in sys.path:
        sys.path.insert(0, str(workspace))

    import prompts.team_prompts  # noqa: F401

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

    import dispatch as _dispatch

    _dispatch.WORKSPACE = tmp_path
    return _dispatch


# ---------------------------------------------------------------------------
# fixture
# ---------------------------------------------------------------------------


@pytest.fixture()
def dispatch_mod(tmp_path: Path) -> types.ModuleType:
    """격리된 WORKSPACE를 사용하는 dispatch 모듈 반환"""
    (tmp_path / "memory").mkdir(parents=True, exist_ok=True)
    (tmp_path / "memory" / "tasks").mkdir(parents=True, exist_ok=True)
    return _load_dispatch_with_workspace(tmp_path)


@pytest.fixture()
def summary_file(tmp_path: Path) -> Path:
    """임시 요약 파일 생성"""
    summary = tmp_path / "session_summary.md"
    summary.write_text("이전 세션에서 A 모듈 구현 완료.\n남은 작업: B 모듈 테스트 작성.", encoding="utf-8")
    return summary


# ---------------------------------------------------------------------------
# mock_dispatch: 실제 cokacdir 호출 없이 dispatch() 내부 로직만 테스트
# ---------------------------------------------------------------------------

_MOCK_PATCHES = [
    "dispatch.subprocess.run",
    "dispatch.generate_task_id",
]


def _make_mock_subprocess() -> MagicMock:
    """성공 응답을 반환하는 subprocess.run mock"""
    mock = MagicMock()
    mock.return_value.returncode = 0
    mock.return_value.stdout = json.dumps({"session_id": "test-session"})
    mock.return_value.stderr = ""
    return mock


# ---------------------------------------------------------------------------
# 1. resume_from=None → task_desc 변경 없음
# ---------------------------------------------------------------------------


class TestResumeFromNone:
    """resume_from이 None이면 기존 동작과 동일해야 한다."""

    def test_task_desc_unchanged_when_resume_from_is_none(self, dispatch_mod: types.ModuleType, tmp_path: Path) -> None:
        original_task_desc = "원래 작업 설명입니다."
        captured_task_desc: list[str] = []

        def _fake_build_prompt(
            team_id: str,
            task_desc: str,
            task_id: str,
            level: str = "normal",
            **kwargs: object,
        ) -> str:
            captured_task_desc.append(task_desc)
            return "fake-prompt"

        mock_subprocess = _make_mock_subprocess()

        with (
            patch.object(dispatch_mod, "build_prompt", side_effect=_fake_build_prompt),
            patch.object(dispatch_mod, "subprocess") as mock_sp,
            patch.object(dispatch_mod, "generate_task_id", return_value="task-1.1"),
            patch("utils.bot_activity.set_bot_status"),
        ):
            mock_sp.run.return_value.returncode = 0
            mock_sp.run.return_value.stdout = json.dumps({"session_id": "test"})
            mock_sp.run.return_value.stderr = ""

            dispatch_mod.dispatch(
                team_id="dev6-team",
                task_desc=original_task_desc,
                resume_from=None,
            )

        assert len(captured_task_desc) == 1
        assert captured_task_desc[0] == original_task_desc


# ---------------------------------------------------------------------------
# 2. resume_from 존재하는 파일 → task_desc 앞에 요약 prepend
# ---------------------------------------------------------------------------


class TestResumeFromExistingFile:
    """resume_from이 존재하는 파일이면 task_desc 앞에 요약을 prepend한다."""

    def test_summary_prepended_to_task_desc(
        self,
        dispatch_mod: types.ModuleType,
        summary_file: Path,
    ) -> None:
        original_task_desc = "새 작업: B 모듈 테스트 작성."
        summary_content = summary_file.read_text(encoding="utf-8")
        expected_prefix = (
            f"## 이전 세션 요약\n" f"아래 요약을 읽고 이어서 작업하세요.\n\n" f"{summary_content}\n\n" f"---\n\n"
        )

        captured_task_desc: list[str] = []

        def _fake_build_prompt(
            team_id: str,
            task_desc: str,
            task_id: str,
            level: str = "normal",
            **kwargs: object,
        ) -> str:
            captured_task_desc.append(task_desc)
            return "fake-prompt"

        with (
            patch.object(dispatch_mod, "build_prompt", side_effect=_fake_build_prompt),
            patch.object(dispatch_mod, "subprocess") as mock_sp,
            patch.object(dispatch_mod, "generate_task_id", return_value="task-1.1"),
            patch("utils.bot_activity.set_bot_status"),
        ):
            mock_sp.run.return_value.returncode = 0
            mock_sp.run.return_value.stdout = json.dumps({"session_id": "test"})
            mock_sp.run.return_value.stderr = ""

            dispatch_mod.dispatch(
                team_id="dev6-team",
                task_desc=original_task_desc,
                resume_from=str(summary_file),
            )

        assert len(captured_task_desc) == 1
        assert captured_task_desc[0].startswith(expected_prefix)
        assert captured_task_desc[0].endswith(original_task_desc)

    def test_full_injected_task_desc_format(
        self,
        dispatch_mod: types.ModuleType,
        summary_file: Path,
    ) -> None:
        """prepend된 task_desc의 전체 형식 검증"""
        original_task_desc = "새 작업: C 모듈 구현."
        summary_content = summary_file.read_text(encoding="utf-8")
        expected_task_desc = (
            f"## 이전 세션 요약\n"
            f"아래 요약을 읽고 이어서 작업하세요.\n\n"
            f"{summary_content}\n\n"
            f"---\n\n"
            f"{original_task_desc}"
        )

        captured_task_desc: list[str] = []

        def _fake_build_prompt(
            team_id: str,
            task_desc: str,
            task_id: str,
            level: str = "normal",
            **kwargs: object,
        ) -> str:
            captured_task_desc.append(task_desc)
            return "fake-prompt"

        with (
            patch.object(dispatch_mod, "build_prompt", side_effect=_fake_build_prompt),
            patch.object(dispatch_mod, "subprocess") as mock_sp,
            patch.object(dispatch_mod, "generate_task_id", return_value="task-1.1"),
            patch("utils.bot_activity.set_bot_status"),
        ):
            mock_sp.run.return_value.returncode = 0
            mock_sp.run.return_value.stdout = json.dumps({"session_id": "test"})
            mock_sp.run.return_value.stderr = ""

            dispatch_mod.dispatch(
                team_id="dev6-team",
                task_desc=original_task_desc,
                resume_from=str(summary_file),
            )

        assert len(captured_task_desc) == 1
        assert captured_task_desc[0] == expected_task_desc


# ---------------------------------------------------------------------------
# 3. resume_from이 존재하지 않는 파일 → error 반환
# ---------------------------------------------------------------------------


class TestResumeFromNonExistentFile:
    """resume_from이 존재하지 않는 파일이면 error를 반환한다."""

    def test_returns_error_when_file_not_found(self, dispatch_mod: types.ModuleType, tmp_path: Path) -> None:
        non_existent_path = str(tmp_path / "no_such_file.md")

        result = dispatch_mod.dispatch(
            team_id="dev6-team",
            task_desc="작업 설명",
            resume_from=non_existent_path,
        )

        assert result["status"] == "error"
        assert "resume_from" in result["message"] or non_existent_path in result["message"]

    def test_error_message_contains_path(self, dispatch_mod: types.ModuleType, tmp_path: Path) -> None:
        non_existent_path = str(tmp_path / "missing_summary.md")

        result = dispatch_mod.dispatch(
            team_id="dev6-team",
            task_desc="작업 설명",
            resume_from=non_existent_path,
        )

        assert result["status"] == "error"
        assert non_existent_path in result["message"]


# ---------------------------------------------------------------------------
# 4. CLI argparse: --resume-from 옵션 파싱
# ---------------------------------------------------------------------------


class TestCliResumeParsing:
    """argparse가 --resume-from 옵션을 정상 파싱하는지 확인한다."""

    def _get_parser(self) -> argparse.ArgumentParser:
        """dispatch.main()에서 사용하는 argparse parser를 재생성하여 반환."""

        parser = argparse.ArgumentParser(description="작업 위임 디스패처")
        team_or_composite = parser.add_mutually_exclusive_group(required=False)
        team_or_composite.add_argument("--team", default=None)
        team_or_composite.add_argument("--composite", default=None)

        task_group = parser.add_mutually_exclusive_group(required=False)
        task_group.add_argument("--task", default=None)
        task_group.add_argument("--task-file", default=None)

        parser.add_argument("--level", default="normal")
        parser.add_argument("--type", default="coding")
        parser.add_argument("--session", default=None)
        parser.add_argument("--project", default=None)
        parser.add_argument("--chain", default=None)
        parser.add_argument("--refresh-map", action=argparse.BooleanOptionalAction, default=True)
        parser.add_argument("--task-id", default=None)
        parser.add_argument("--phases", type=int, default=None)
        parser.add_argument("--force", action="store_true", default=False)
        parser.add_argument("--resume-from", default=None, dest="resume_from")
        return parser

    def test_resume_from_parsed_correctly(self, tmp_path: Path) -> None:
        """--resume-from 값이 args.resume_from에 올바르게 저장된다."""
        summary_path = str(tmp_path / "summary.md")
        parser = self._get_parser()
        args = parser.parse_args(["--team", "dev6-team", "--task", "테스트", "--resume-from", summary_path])
        assert args.resume_from == summary_path

    def test_resume_from_default_is_none(self) -> None:
        """--resume-from 미지정 시 기본값은 None이다."""
        parser = self._get_parser()
        args = parser.parse_args(["--team", "dev6-team", "--task", "테스트"])
        assert args.resume_from is None

    def test_resume_from_can_be_used_with_task(self, tmp_path: Path) -> None:
        """--resume-from은 --task와 함께 사용 가능하다."""
        summary_path = str(tmp_path / "summary.md")
        parser = self._get_parser()
        args = parser.parse_args(["--team", "dev6-team", "--task", "작업 내용", "--resume-from", summary_path])
        assert args.task == "작업 내용"
        assert args.resume_from == summary_path

    def test_resume_from_can_be_used_with_task_file(self, tmp_path: Path) -> None:
        """--resume-from은 --task-file과 함께 사용 가능하다."""
        summary_path = str(tmp_path / "summary.md")
        task_file_path = str(tmp_path / "task.md")
        parser = self._get_parser()
        args = parser.parse_args(["--team", "dev6-team", "--task-file", task_file_path, "--resume-from", summary_path])
        assert args.task_file == task_file_path
        assert args.resume_from == summary_path

    def test_dispatch_main_has_resume_from_option(self, tmp_path: Path) -> None:
        """실제 dispatch.main()의 argparse에 --resume-from 옵션이 존재한다."""
        workspace = _WORKSPACE
        if str(workspace) not in sys.path:
            sys.path.insert(0, str(workspace))

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

        import dispatch as _dispatch

        summary_path = str(tmp_path / "summary.md")

        # dispatch.main() 내부와 동일한 parser 재현하여 --resume-from 포함 여부 확인
        # sys.argv를 임시 교체
        original_argv = sys.argv
        try:
            sys.argv = [
                "dispatch.py",
                "--team",
                "dev6-team",
                "--task",
                "테스트",
                "--resume-from",
                summary_path,
            ]
            # main()을 직접 호출하면 실제 dispatch가 실행되므로
            # argparse 파싱 부분만 분리해서 검증:
            # dispatch 모듈에 --resume-from 파라미터가 있는지 확인
            import inspect

            sig = inspect.signature(_dispatch.dispatch)
            assert "resume_from" in sig.parameters, "dispatch() 함수에 resume_from 파라미터가 없습니다."
        finally:
            sys.argv = original_argv
