"""
test_3docs_e2e.py — 3문서 2유형 체계 End-to-End 통합 테스트

Phase 1~5 구현 완료된 3문서 체계(plan.md / context-notes.md / checklist.md)의
전체 흐름을 4개 그룹으로 검증한다:
  1. dispatch → 3문서 자동 생성
  2. team_prompts 3문서 지침 포함 여부
  3. QC verifier 3문서 검증 (PASS / WARN / SKIP / FAIL 시나리오)
  4. DIRECT-WORKFLOW.md 3문서 단계 존재 확인

기존 tests/test_dispatch_task_docs.py 와 독립 파일로 작성되어 충돌 없이 병렬 실행 가능.
"""

import os
import sys
import types
from pathlib import Path

import pytest
import yaml

# ---------------------------------------------------------------------------
# 상수
# ---------------------------------------------------------------------------

REAL_WORKSPACE = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
REAL_TEMPLATE_DIR = REAL_WORKSPACE / "prompts" / "templates" / "task-docs"
TEMPLATE_FILES = [
    "plan.template.md",
    "context-notes.template.md",
    "checklist.template.md",
]

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


def _load_dispatch_with_workspace(tmp_path: Path) -> types.ModuleType:
    """dispatch 모듈을 tmp_path를 WORKSPACE로 설정하여 격리 로드한다."""
    workspace = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
    if str(workspace) not in sys.path:
        sys.path.insert(0, str(workspace))

    # prompts 패키지를 먼저 로드 (dispatch 의존)
    import prompts.team_prompts  # type: ignore[reportMissingImports,reportUnusedImport]  # noqa: F401

    # dispatch 모듈을 새로 로드 (캐시 제거)
    for mod_name in list(sys.modules.keys()):
        if mod_name == "dispatch":
            del sys.modules[mod_name]

    import dispatch as _dispatch  # type: ignore[reportMissingImports]

    # WORKSPACE를 tmp_path로 교체
    _dispatch.WORKSPACE = tmp_path
    return _dispatch


# ---------------------------------------------------------------------------
# fixture: 격리된 dispatch 모듈 + 템플릿 파일 세팅
# ---------------------------------------------------------------------------


@pytest.fixture()
def dispatch_mod(tmp_path: Path) -> types.ModuleType:
    """격리된 WORKSPACE를 사용하는 dispatch 모듈 반환 + 템플릿 파일 복사."""
    # memory 디렉토리 구조 생성
    (tmp_path / "memory").mkdir(parents=True, exist_ok=True)
    (tmp_path / "memory" / "tasks").mkdir(parents=True, exist_ok=True)

    # 템플릿 디렉토리 생성 및 실제 템플릿 복사
    tmpl_dir = tmp_path / "prompts" / "templates" / "task-docs"
    tmpl_dir.mkdir(parents=True, exist_ok=True)
    for fname in TEMPLATE_FILES:
        src = REAL_TEMPLATE_DIR / fname
        if src.exists():
            (tmpl_dir / fname).write_text(src.read_text(encoding="utf-8"), encoding="utf-8")

    real_workspace = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
    # 기존 dispatch 모듈을 보존 (다른 테스트의 함수 __globals__ 보호)
    _original_dispatch = sys.modules.get("dispatch")
    mod = _load_dispatch_with_workspace(tmp_path)
    yield mod
    # 테스트 후 WORKSPACE를 실제 경로로 복원하여 다른 테스트 격리
    mod.WORKSPACE = real_workspace
    # sys.modules["dispatch"]를 원래 모듈로 복원
    if _original_dispatch is not None:
        sys.modules["dispatch"] = _original_dispatch


# ---------------------------------------------------------------------------
# 그룹 1: dispatch → 3문서 자동 생성 테스트
# ---------------------------------------------------------------------------


class TestDispatchCreateTaskDocs:
    """dispatch._create_task_docs() E2E 통합 테스트"""

    def test_critical_level_creates_directory_and_three_files(
        self, dispatch_mod: types.ModuleType, tmp_path: Path
    ) -> None:
        """level=3(critical)에서 tasks/{task_id}/ 디렉토리와 3문서 파일이 생성된다.

        _create_task_docs(task_id, level=3) 호출 후
        memory/plans/tasks/{task_id}/ 경로에 plan.md, context-notes.md,
        checklist.md 세 파일이 모두 존재해야 한다.
        """
        task_id = "task-9001"
        result = dispatch_mod._create_task_docs(task_id, level=3)

        assert result is not None, "_create_task_docs가 None을 반환했다 (Path 반환 기대)"
        assert isinstance(result, Path), "반환값이 Path 타입이어야 한다"

        expected_dir = tmp_path / "memory" / "plans" / "tasks" / task_id
        assert expected_dir.exists(), f"디렉토리가 생성되지 않았다: {expected_dir}"
        assert expected_dir.is_dir(), "디렉토리가 파일이 아닌 폴더여야 한다"

        for fname in ("plan.md", "context-notes.md", "checklist.md"):
            fpath = expected_dir / fname
            assert fpath.exists(), f"3문서 파일이 생성되지 않았다: {fpath}"

    def test_yaml_frontmatter_has_required_fields(
        self, dispatch_mod: types.ModuleType, tmp_path: Path
    ) -> None:
        """생성된 3문서 각각의 YAML frontmatter에 필수 필드 4개가 존재한다.

        task_id, type, scope, status 필드가 있어야 하며,
        {task_id} / {date} 플레이스홀더가 실제 값으로 치환되어야 한다.
        """
        task_id = "task-9002"
        dispatch_mod._create_task_docs(task_id, level=3)

        expected_dir = tmp_path / "memory" / "plans" / "tasks" / task_id

        for fname in ("plan.md", "context-notes.md", "checklist.md"):
            fpath = expected_dir / fname
            assert fpath.exists(), f"파일이 없다: {fpath}"

            content = fpath.read_text(encoding="utf-8")
            assert content.startswith("---"), f"{fname}: YAML frontmatter(---) 없음"

            end_idx = content.index("---", 3)
            yaml_text = content[3:end_idx].strip()
            data = yaml.safe_load(yaml_text)

            for field in ("task_id", "type", "scope", "status"):
                assert field in data, f"{fname}: 필수 YAML 필드 '{field}' 없음"

            assert data["task_id"] == task_id, (
                f"{fname}: task_id 치환 실패 — 기대: {task_id}, 실제: {data['task_id']}"
            )
            assert "{task_id}" not in content, f"{fname}: {{task_id}} 플레이스홀더 미치환"
            assert "{date}" not in content, f"{fname}: {{date}} 플레이스홀더 미치환"

    def test_normal_level_does_not_call_create_task_docs(
        self, dispatch_mod: types.ModuleType, tmp_path: Path
    ) -> None:
        """dispatch 내부 로직에서 level=2(normal)일 때는 _create_task_docs가 호출되지 않는다.

        dispatch()는 내부 레벨 정수가 3 이상일 때만 _create_task_docs를 호출한다.
        level<3 조건을 재현하여 호출 횟수가 0임을 검증한다.
        """
        task_id = "task-9003"
        _level_to_int = {"normal": 2, "critical": 3, "security": 4}
        docs_level = _level_to_int.get("normal", 2)

        called: list[tuple[str, int]] = []
        original_fn = dispatch_mod._create_task_docs

        def _tracking_fn(tid: str, lv: int) -> object:
            called.append((tid, lv))
            return original_fn(tid, lv)

        setattr(dispatch_mod, "_create_task_docs", _tracking_fn)
        try:
            # dispatch 내부 조건 재현: level >= 3 일 때만 호출
            if docs_level >= 3:
                dispatch_mod._create_task_docs(task_id, docs_level)
        finally:
            setattr(dispatch_mod, "_create_task_docs", original_fn)

        assert len(called) == 0, "normal(Lv.2) 레벨에서 _create_task_docs가 호출되면 안 된다"

        # 디렉토리도 생성되지 않아야 한다
        expected_dir = tmp_path / "memory" / "plans" / "tasks" / task_id
        assert not expected_dir.exists(), (
            f"normal 레벨에서 3문서 디렉토리가 생성되면 안 된다: {expected_dir}"
        )


# ---------------------------------------------------------------------------
# 그룹 2: team_prompts 3문서 지침 포함 테스트
# ---------------------------------------------------------------------------


class TestTeamPromptsThreeDocsSection:
    """team_prompts._build_three_docs_section() E2E 통합 테스트"""

    @pytest.fixture(autouse=True)
    def _import_build_fn(self) -> None:
        """_build_three_docs_section 함수를 임포트한다."""
        workspace = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
        if str(workspace) not in sys.path:
            sys.path.insert(0, str(workspace))
        from prompts.team_prompts import _build_three_docs_section  # type: ignore[reportMissingImports]

        self._build = _build_three_docs_section

    def test_critical_level_contains_three_docs_guide_header(self) -> None:
        """level='critical'이면 반환 문자열에 '3문서 활용 지침' 텍스트가 포함된다.

        _build_three_docs_section("task-9001", "critical") 호출 결과에
        '3문서 활용 지침' 문자열이 반드시 포함되어야 한다.
        """
        result = self._build("task-9001", "critical")
        assert "3문서 활용 지침" in result, (
            "critical 레벨에서 '3문서 활용 지침' 텍스트가 없다"
        )

    def test_critical_level_contains_update_before_report(self) -> None:
        """level='critical'이면 '보고서 작성 전 3문서 업데이트' 문구가 포함된다.

        _build_three_docs_section("task-9001", "critical") 호출 결과에
        보고서 작성 전 3문서 업데이트를 안내하는 텍스트가 포함되어야 한다.
        """
        result = self._build("task-9001", "critical")
        assert "보고서 작성 전 3문서 업데이트" in result, (
            "critical 레벨에서 '보고서 작성 전 3문서 업데이트' 안내 문구가 없다"
        )

    def test_normal_level_returns_empty_string(self) -> None:
        """level='normal'이면 빈 문자열('')을 반환한다.

        normal(Lv.2) 수준 작업에서는 3문서 지침이 불필요하므로
        _build_three_docs_section 이 빈 문자열을 반환해야 한다.
        """
        result = self._build("task-9004", "normal")
        assert result == "", (
            f"normal 레벨에서 빈 문자열이 반환되어야 한다. 실제: {repr(result)}"
        )


# ---------------------------------------------------------------------------
# 그룹 3: QC verifier 3문서 검증 테스트
# ---------------------------------------------------------------------------

# three_docs_check.verify 임포트
sys.path.insert(0, "/home/jay/workspace/teams/dev6/qc/verifiers")
from three_docs_check import verify as _three_docs_verify  # type: ignore[reportMissingImports]  # noqa: E402


def _make_plan_md(task_id: str, status: str = "in-progress") -> str:
    """테스트용 plan.md 내용을 생성한다."""
    return (
        f"---\n"
        f"task_id: {task_id}\n"
        f"type: plan\n"
        f"scope: task\n"
        f"created: 2026-04-16\n"
        f"updated: 2026-04-16\n"
        f"status: {status}\n"
        f"---\n\n"
        f"# 계획서: {task_id}\n\n"
        f"## 목표\n\n"
        f"E2E 통합 테스트 완성\n\n"
        f"## 범위\n\n"
        f"3문서 체계 전체 검증\n"
    )


def _make_context_notes_md(task_id: str, placeholder: bool = False) -> str:
    """테스트용 context-notes.md 내용을 생성한다."""
    body = "실제 결정 근거 내용" if not placeholder else "결정 근거: [미입력 내용]"
    return (
        f"---\n"
        f"task_id: {task_id}\n"
        f"type: context\n"
        f"scope: task\n"
        f"created: 2026-04-16\n"
        f"updated: 2026-04-16\n"
        f"status: in-progress\n"
        f"---\n\n"
        f"# 맥락 노트: {task_id}\n\n"
        f"## 결정 근거\n\n"
        f"{body}\n"
    )


def _make_checklist_md(task_id: str, completed: int = 3, total: int = 4) -> str:
    """테스트용 checklist.md 내용을 생성한다. completed/total 비율로 완료율 설정."""
    lines = []
    for i in range(total):
        if i < completed:
            lines.append(f"- [x] 항목 {i + 1}")
        else:
            lines.append(f"- [ ] 항목 {i + 1}")
    checklist_body = "\n".join(lines)
    return (
        f"---\n"
        f"task_id: {task_id}\n"
        f"type: checklist\n"
        f"scope: task\n"
        f"created: 2026-04-16\n"
        f"updated: 2026-04-16\n"
        f"status: in-progress\n"
        f"---\n\n"
        f"# 체크리스트: {task_id}\n\n"
        f"{checklist_body}\n"
    )


def _write_three_docs(
    workspace: Path,
    task_id: str,
    plan_content: str,
    context_content: str,
    checklist_content: str,
) -> Path:
    """tmp workspace에 3문서 파일을 기록하고 디렉토리 경로를 반환한다."""
    docs_dir = workspace / "memory" / "plans" / "tasks" / task_id
    docs_dir.mkdir(parents=True, exist_ok=True)
    (docs_dir / "plan.md").write_text(plan_content, encoding="utf-8")
    (docs_dir / "context-notes.md").write_text(context_content, encoding="utf-8")
    (docs_dir / "checklist.md").write_text(checklist_content, encoding="utf-8")
    return docs_dir


class TestQcVerifierThreeDocs:
    """three_docs_check.verify() E2E 통합 테스트 — PASS / WARN / SKIP / FAIL 시나리오"""

    def test_pass_scenario(self, tmp_path: Path) -> None:
        """PASS 시나리오: 3문서 존재 + YAML 유효 + status!=draft + 체크리스트 50%+.

        3문서가 모두 존재하고, YAML frontmatter 필수 필드가 있으며,
        plan.md status가 draft가 아니고, checklist 완료율이 50% 이상이고,
        플레이스홀더가 없으면 결과 status가 'PASS'여야 한다.
        """
        task_id = "task-e2e-pass"
        _write_three_docs(
            workspace=tmp_path,
            task_id=task_id,
            plan_content=_make_plan_md(task_id, status="in-progress"),
            context_content=_make_context_notes_md(task_id, placeholder=False),
            checklist_content=_make_checklist_md(task_id, completed=3, total=4),
        )

        result = _three_docs_verify(task_id=task_id, workspace=str(tmp_path))
        assert result["status"] == "PASS", (
            f"PASS 시나리오에서 상태가 PASS여야 한다. 실제: {result['status']}\n"
            f"메시지: {result['message']}"
        )

    def test_warn_scenario_draft_status(self, tmp_path: Path) -> None:
        """WARN 시나리오: plan.md status=draft면 WARN이 반환된다.

        3문서가 모두 존재하더라도 plan.md의 status 필드가 'draft'이면
        결과 status가 'WARN'이어야 한다.
        """
        task_id = "task-e2e-warn-draft"
        _write_three_docs(
            workspace=tmp_path,
            task_id=task_id,
            plan_content=_make_plan_md(task_id, status="draft"),
            context_content=_make_context_notes_md(task_id, placeholder=False),
            checklist_content=_make_checklist_md(task_id, completed=3, total=4),
        )

        result = _three_docs_verify(task_id=task_id, workspace=str(tmp_path))
        assert result["status"] == "WARN", (
            f"plan.md status=draft이면 WARN이어야 한다. 실제: {result['status']}\n"
            f"메시지: {result['message']}"
        )

    def test_warn_scenario_placeholder_exists(self, tmp_path: Path) -> None:
        """WARN 시나리오: 플레이스홀더 미치환 잔존 시 WARN이 반환된다.

        context-notes.md 본문에 '[미입력 내용]' 형태의 플레이스홀더가 남아 있으면
        결과 status가 'WARN'이어야 한다.
        """
        task_id = "task-e2e-warn-ph"
        _write_three_docs(
            workspace=tmp_path,
            task_id=task_id,
            plan_content=_make_plan_md(task_id, status="in-progress"),
            context_content=_make_context_notes_md(task_id, placeholder=True),
            checklist_content=_make_checklist_md(task_id, completed=3, total=4),
        )

        result = _three_docs_verify(task_id=task_id, workspace=str(tmp_path))
        assert result["status"] == "WARN", (
            f"플레이스홀더 잔존 시 WARN이어야 한다. 실제: {result['status']}\n"
            f"메시지: {result['message']}"
        )

    def test_skip_scenario_no_directory(self, tmp_path: Path) -> None:
        """SKIP 시나리오: 3문서 디렉토리 자체가 없으면 SKIP이 반환된다.

        Lv.2 이하 작업처럼 memory/plans/tasks/{task_id}/ 디렉토리가 없으면
        결과 status가 'SKIP'이어야 한다.
        """
        task_id = "task-e2e-skip-nodirectory"
        # 디렉토리를 생성하지 않음

        result = _three_docs_verify(task_id=task_id, workspace=str(tmp_path))
        assert result["status"] == "SKIP", (
            f"디렉토리 없을 때 SKIP이어야 한다. 실제: {result['status']}\n"
            f"메시지: {result['message']}"
        )

    def test_fail_scenario_missing_file(self, tmp_path: Path) -> None:
        """FAIL 시나리오: 3문서 디렉토리는 있으나 파일 중 하나가 누락되면 FAIL이 반환된다.

        memory/plans/tasks/{task_id}/ 디렉토리는 존재하지만
        checklist.md가 없는 경우 결과 status가 'FAIL'이어야 한다.
        """
        task_id = "task-e2e-fail-missing"
        docs_dir = tmp_path / "memory" / "plans" / "tasks" / task_id
        docs_dir.mkdir(parents=True, exist_ok=True)

        # plan.md와 context-notes.md만 생성, checklist.md 누락
        (docs_dir / "plan.md").write_text(
            _make_plan_md(task_id, status="in-progress"), encoding="utf-8"
        )
        (docs_dir / "context-notes.md").write_text(
            _make_context_notes_md(task_id, placeholder=False), encoding="utf-8"
        )
        # checklist.md는 의도적으로 생성하지 않음

        result = _three_docs_verify(task_id=task_id, workspace=str(tmp_path))
        assert result["status"] == "FAIL", (
            f"파일 누락 시 FAIL이어야 한다. 실제: {result['status']}\n"
            f"메시지: {result['message']}"
        )


# ---------------------------------------------------------------------------
# 그룹 4: DIRECT-WORKFLOW.md 3문서 단계 존재 확인
# ---------------------------------------------------------------------------

DIRECT_WORKFLOW_PATH = REAL_WORKSPACE / "prompts" / "DIRECT-WORKFLOW.md"


class TestDirectWorkflowThreeDocsSteps:
    """DIRECT-WORKFLOW.md에 3문서 관련 단계 텍스트가 존재하는지 확인한다."""

    @pytest.fixture(autouse=True)
    def _read_workflow_content(self) -> None:
        """DIRECT-WORKFLOW.md 파일 내용을 읽어 인스턴스 변수에 저장한다."""
        assert DIRECT_WORKFLOW_PATH.exists(), (
            f"DIRECT-WORKFLOW.md 파일이 없다: {DIRECT_WORKFLOW_PATH}"
        )
        self._content = DIRECT_WORKFLOW_PATH.read_text(encoding="utf-8")

    def test_step_1_2_three_docs_check_exists(self) -> None:
        """DIRECT-WORKFLOW.md에 Step 1.2 '3문서 확인' 텍스트가 존재한다.

        워크플로우 1.2단계에 '3문서 확인' 지시가 명시되어 있어야
        팀장이 작업 시작 시 3문서를 확인한다는 것을 알 수 있다.
        """
        assert "3문서 확인" in self._content, (
            "DIRECT-WORKFLOW.md에 'Step 1.2 3문서 확인' 텍스트가 없다"
        )

    def test_step_5_2_three_docs_update_exists(self) -> None:
        """DIRECT-WORKFLOW.md에 Step 5.2 '3문서 업데이트' 텍스트가 존재한다.

        워크플로우 5.2단계에 '3문서 업데이트' 지시가 명시되어 있어야
        작업 완료 전 3문서를 갱신하는 절차가 강제된다.
        """
        assert "3문서 업데이트" in self._content, (
            "DIRECT-WORKFLOW.md에 'Step 5.2 3문서 업데이트' 텍스트가 없다"
        )

    def test_step_5_3_three_docs_verify_forced_exists(self) -> None:
        """DIRECT-WORKFLOW.md에 Step 5.3 '3문서 검증 강제' 텍스트가 존재한다.

        워크플로우 5.3단계에 '3문서 검증 강제' 지시가 명시되어 있어야
        .done 생성 전 QC verifier 검증이 강제됨을 명확히 한다.
        """
        assert "3문서 검증 강제" in self._content, (
            "DIRECT-WORKFLOW.md에 'Step 5.3 3문서 검증 강제' 텍스트가 없다"
        )
