"""tests/verify/test_taskctl_verify.py — taskctl_verify.py 11 검사 시나리오 테스트.

task-2459 Phase 2-C / dev5팀 닌기르수 (테스터).

scripts/taskctl_verify.py 의 11 개 검사를 임시 worktree + lock + task md 로
재현하여 PASS / FAIL / WARN / N/A 동작을 검증.

원칙:
  - 모듈 코드 read-only.
  - subprocess 로만 호출.
  - workspace 와 cwd 는 독립적 — `.worktrees/<id>-<bot>` 경로 패턴을 cwd 로 사용.
"""
from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

import pytest


REPO_ROOT = Path(__file__).resolve().parents[2]
VERIFY = REPO_ROOT / "scripts" / "taskctl_verify.py"
DETECTOR_SRC = REPO_ROOT / "scripts" / "mixed_commit_detector.py"


# ---------------------------------------------------------------------------
# 헬퍼
# ---------------------------------------------------------------------------

def _git(*args: str, cwd: Path) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["git", *args], cwd=str(cwd), capture_output=True, text=True, check=False
    )


def _make_task_md(workspace: Path, task_id: str, allowed: list[str]) -> Path:
    """task md 파일을 yaml 블록 포함하여 작성."""
    p = workspace / "memory" / "tasks" / f"{task_id}.md"
    p.parent.mkdir(parents=True, exist_ok=True)
    paths_yaml = "\n".join(f"    - \"{x}\"" for x in allowed)
    p.write_text(
        f"""# {task_id}

```yaml
allowed_resources:
  paths:
{paths_yaml}
```
""",
        encoding="utf-8",
    )
    return p


def setup_full_repo(
    tmp_path: Path,
    task_id: str = "task-2459",
    bot: str = "dev5",
) -> dict:
    """테스트용 전체 셋업.

    Layout (workspace 자체가 git repo, .worktrees 는 같은 repo 내 하위 디렉토리):
      tmp_path/
        workspace/                       <- WORKSPACE_ROOT, git repo
          .git/
          .tasks/locks/<task>.lock
          memory/tasks/<task>.md
          memory/events/
          scripts/mixed_commit_detector.py  (실제 detector 복사)
          src/                            (allowed_resources 매치용)
          .worktrees/<task>-<bot>/        <- cwd. 같은 repo 의 하위 디렉토리.

    git worktree 분리 모킹 대신 단일 repo 의 subdir 로 구성.
    git 명령은 어떤 cwd 에서도 같은 repo 를 인식한다.
    """
    workspace = tmp_path / "workspace"
    workspace.mkdir()

    # workspace 자체를 git repo 로
    _git("init", "-q", "-b", "main", cwd=workspace)
    _git("config", "user.email", "test@example.com", cwd=workspace)
    _git("config", "user.name", "tester", cwd=workspace)
    _git("config", "commit.gpgsign", "false", cwd=workspace)

    # .tasks/locks
    locks = workspace / ".tasks" / "locks"
    locks.mkdir(parents=True)
    (locks / f"{task_id}.lock").write_text("ok\n", encoding="utf-8")

    # memory/events
    (workspace / "memory" / "events").mkdir(parents=True)

    # scripts/mixed_commit_detector.py (실제 모듈을 workspace 내에 복사하여
    # check_mixed_commit 이 발견하도록 함). taskctl_verify 는
    # workspace/scripts/mixed_commit_detector.py 를 찾는다.
    scripts_dir = workspace / "scripts"
    scripts_dir.mkdir(parents=True)
    detector_dst = scripts_dir / "mixed_commit_detector.py"
    detector_dst.write_text(DETECTOR_SRC.read_text(encoding="utf-8"), encoding="utf-8")

    # task md (allowed_resources 포함)
    _make_task_md(
        workspace,
        task_id,
        ["src/**", "memory/**", "tests/**", "scripts/**", ".tasks/**"],
    )

    # worktree 디렉토리 (같은 repo 내) — placeholder 를 두어 dirty_tree 검사가
    # 새 untracked 파일을 정확히 식별할 수 있게 함 (placeholder 자체는 base 에 포함).
    worktree = workspace / ".worktrees" / f"{task_id}-{bot}"
    worktree.mkdir(parents=True)
    (worktree / ".keep").write_text("keep\n", encoding="utf-8")

    # 초기 commit (origin/main 의 base) — 모든 setup 파일 포함
    _git("add", "-A", cwd=workspace)
    _git("commit", "-q", "-m", "base: initial setup", cwd=workspace)

    # origin/main ref 시뮬레이션
    base_sha = _git("rev-parse", "HEAD", cwd=workspace).stdout.strip()
    _git("update-ref", "refs/remotes/origin/main", base_sha, cwd=workspace)

    # task 브랜치로 전환
    branch = f"task/{task_id}-{bot}"
    _git("checkout", "-q", "-b", branch, cwd=workspace)

    # task 브랜치에 정상 commit 1개 추가 (allowed scope 내)
    src_dir = workspace / "src"
    src_dir.mkdir(exist_ok=True)
    (src_dir / "module.py").write_text("# implementation\n", encoding="utf-8")
    _git("add", "src/module.py", cwd=workspace)
    _git("commit", "-q", "-m", f"[{task_id}] feat: add module", cwd=workspace)

    return {
        "workspace": workspace,
        "worktree": worktree,
        "task_id": task_id,
        "bot": bot,
        "branch": branch,
        "base_sha": base_sha,
    }


def _commit_all(workspace: Path, message: str) -> None:
    """현재 staged/untracked 모두 commit (dirty_tree 정리)."""
    _git("add", "-A", cwd=workspace)
    _git("commit", "-q", "-m", message, cwd=workspace)


def run_verify(
    task_id: str,
    *,
    bot: str | None = None,
    workspace: Path,
    cwd: Path,
    json_mode: bool = True,
    extra: tuple[str, ...] = (),
) -> subprocess.CompletedProcess:
    cmd = [sys.executable, str(VERIFY), task_id, "--workspace", str(workspace)]
    if bot:
        cmd += ["--bot", bot]
    if json_mode:
        cmd += ["--json"]
    cmd += list(extra)
    return subprocess.run(
        cmd, cwd=str(cwd), capture_output=True, text=True, check=False
    )


def _parse_payload(res: subprocess.CompletedProcess) -> dict:
    assert res.stdout, f"stdout 비어있음: stderr={res.stderr}"
    return json.loads(res.stdout)


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

@pytest.fixture
def full_setup(tmp_path: Path):
    return setup_full_repo(tmp_path)


# ---------------------------------------------------------------------------
# PASS 케이스 (모든 필수 검사 PASS)
# ---------------------------------------------------------------------------

def test_all_pass_baseline(full_setup):
    """모든 필수 검사 PASS, 옵션은 N/A → overall=PASS, exit 0."""
    s = full_setup
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["overall"] == "PASS", (
        f"overall != PASS\nresults={payload['results']}\n"
        f"fail_reasons={payload['details'].get('fail_reasons')}"
    )
    assert res.returncode == 0
    # 필수 검사 모두 PASS
    for chk in (
        "start_lock",
        "branch_match",
        "worktree_path",
        "cancelled_check",
        "dirty_tree",
        "changed_paths",
        "scope_matrix",
        "mixed_commit",
    ):
        assert payload["results"][chk] == "PASS", (
            f"{chk} != PASS — actual {payload['results'][chk]}"
        )
    # 옵션은 N/A
    assert payload["results"]["handoff_chain"] == "N/A"
    assert payload["results"]["qc_report_guard"] == "N/A"
    assert payload["results"]["guard_sh"] == "N/A"


# ---------------------------------------------------------------------------
# FAIL 케이스 (대표 5종)
# ---------------------------------------------------------------------------

def test_fail_start_lock(full_setup):
    """1. start_lock FAIL: lock 파일 삭제."""
    s = full_setup
    (s["workspace"] / ".tasks" / "locks" / f"{s['task_id']}.lock").unlink()
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["start_lock"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1


def test_fail_branch_match(full_setup):
    """2. branch_match FAIL: 다른 브랜치 체크아웃."""
    s = full_setup
    _git("checkout", "-q", "-b", "wrong-branch", cwd=s["worktree"])
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["branch_match"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1


def test_fail_cancelled_check(full_setup):
    """3. cancelled_check FAIL: .cancelled 마커 생성."""
    s = full_setup
    cancel = s["workspace"] / "memory" / "events" / f"{s['task_id']}.cancelled"
    cancel.write_text("cancelled by chairman\n", encoding="utf-8")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["cancelled_check"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1


def test_fail_dirty_tree(full_setup):
    """4. dirty_tree FAIL: untracked 파일 추가."""
    s = full_setup
    (s["worktree"] / "stray.txt").write_text("untracked\n", encoding="utf-8")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["dirty_tree"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1
    assert any("stray.txt" in f for f in payload["details"]["dirty_files"])


def test_fail_scope_matrix(full_setup):
    """5. scope_matrix FAIL: allowed 외 경로에 commit."""
    s = full_setup
    # allowed = src/**, memory/**, tests/** — `forbidden/` 는 violation
    (s["worktree"] / "forbidden").mkdir()
    (s["worktree"] / "forbidden" / "x.py").write_text("x\n", encoding="utf-8")
    _git("add", "forbidden/x.py", cwd=s["worktree"])
    _git(
        "commit", "-q", "-m", f"[{s['task_id']}] add forbidden",
        cwd=s["worktree"],
    )
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["scope_matrix"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1
    violations = payload["details"]["scope_violations"]
    assert any("forbidden/x.py" in v for v in violations)


# ---------------------------------------------------------------------------
# 옵션 검사 — N/A / WARN
# ---------------------------------------------------------------------------

def test_optional_checks_na_when_missing(full_setup):
    """handoff/guard.sh/qc 보고서 부재 → 옵션 검사는 N/A, overall PASS."""
    s = full_setup
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["handoff_chain"] == "N/A"
    assert payload["results"]["qc_report_guard"] == "N/A"
    assert payload["results"]["guard_sh"] == "N/A"
    # 모두 N/A 라도 필수 PASS 면 overall PASS
    assert payload["overall"] == "PASS"
    assert res.returncode == 0


def test_warn_overall_when_scope_matrix_warn(full_setup):
    """WARN 시나리오: task md 에 ``allowed_resources`` 블록 부재 (구버전 호환) →
    scope_matrix WARN, FAIL 0건 → overall=WARN, exit 2.

    spec 4.1 우선순위: FAIL 0 + WARN 1+ → exit 2 / overall=WARN.
    Codex 사전 검증 후 정책 변경: task md 부재/읽기 실패/paths 빈 리스트 = FAIL,
    allowed_resources 블록 자체가 없는 구버전 task md 만 WARN.
    """
    s = full_setup
    # task md 를 allowed_resources 블록 없는 구버전 형태로 덮어쓴다.
    task_md = s["workspace"] / "memory" / "tasks" / f"{s['task_id']}.md"
    task_md.write_text(
        f"# {s['task_id']}\n\nlegacy task md without allowed_resources block.\n",
        encoding="utf-8",
    )
    _commit_all(s["workspace"], f"[{s['task_id']}] downgrade task md (legacy)")

    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["scope_matrix"] == "WARN", (
        f"results={payload['results']}\n"
        f"fail_reasons={payload['details'].get('fail_reasons')}"
    )
    # FAIL 검사 없어야
    assert "FAIL" not in payload["results"].values()
    assert payload["overall"] == "WARN"
    assert res.returncode == 2


def test_fail_scope_matrix_task_md_missing(full_setup):
    """task md 부재 시 scope_matrix FAIL (변경된 정책).

    Codex 결함 #2 — task md 부재 / 읽기 실패 / paths 빈 리스트는 차단(FAIL)이
    설계 의도다. 이전 구현은 WARN 으로 처리했으나 현 spec 2.7 에 따라 FAIL.
    """
    s = full_setup
    task_md = s["workspace"] / "memory" / "tasks" / f"{s['task_id']}.md"
    task_md.unlink()
    _commit_all(s["workspace"], f"[{s['task_id']}] remove task md")

    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["scope_matrix"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1
    # top-level fail_reasons 에 사유 노출
    assert any("scope_matrix" in r for r in payload["fail_reasons"])


def test_warn_optional_handoff_invalid(full_setup):
    """handoff JSON 부서진 schema → handoff_chain FAIL → overall FAIL."""
    s = full_setup
    handoff = s["workspace"] / "memory" / "handoffs" / f"{s['task_id']}.json"
    handoff.parent.mkdir(parents=True, exist_ok=True)
    # 필수 필드 누락
    handoff.write_text(json.dumps({"task_id": s["task_id"]}), encoding="utf-8")
    _commit_all(s["workspace"], f"[{s['task_id']}] add handoff (invalid)")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["handoff_chain"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1


def test_qc_report_pass_verdict(full_setup):
    """qc 보고서 PASS verdict → qc_report_guard=PASS, overall PASS."""
    s = full_setup
    report = s["workspace"] / "memory" / "reports" / f"{s['task_id']}.md"
    report.parent.mkdir(parents=True, exist_ok=True)
    report.write_text(
        "# QC Report\n\nqc_verdict: PASS\n", encoding="utf-8"
    )
    # dirty_tree 통과를 위해 commit
    _commit_all(s["workspace"], f"[{s['task_id']}] add qc report")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["qc_report_guard"] == "PASS"
    assert payload["overall"] == "PASS"
    assert res.returncode == 0


def test_qc_report_fail_verdict(full_setup):
    """qc 보고서 FAIL verdict → qc_report_guard=FAIL, overall FAIL."""
    s = full_setup
    report = s["workspace"] / "memory" / "reports" / f"{s['task_id']}.md"
    report.parent.mkdir(parents=True, exist_ok=True)
    report.write_text(
        "# QC Report\n\nqc_verdict: FAIL\n", encoding="utf-8"
    )
    _commit_all(s["workspace"], f"[{s['task_id']}] add qc report")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["qc_report_guard"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1


# ---------------------------------------------------------------------------
# evidence 파일 검증
# ---------------------------------------------------------------------------

def test_evidence_file_written(full_setup):
    """--json 미사용 시 evidence 파일이 .tasks/evidence/<id>/verify-*.json 에 저장됨."""
    s = full_setup
    res = run_verify(
        s["task_id"],
        bot=s["bot"],
        workspace=s["workspace"],
        cwd=s["worktree"],
        json_mode=False,  # evidence 파일 저장
    )
    assert res.returncode == 0, f"stdout={res.stdout}\nstderr={res.stderr}"
    summary = json.loads(res.stdout)
    assert summary["overall"] == "PASS"
    assert "evidence" in summary

    ev_dir = s["workspace"] / ".tasks" / "evidence" / s["task_id"]
    assert ev_dir.exists()
    evs = sorted(ev_dir.glob("verify-*.json"))
    assert len(evs) >= 1

    payload = json.loads(evs[-1].read_text())
    # 11 검사 키 확인
    expected_keys = {
        "start_lock",
        "branch_match",
        "worktree_path",
        "cancelled_check",
        "dirty_tree",
        "changed_paths",
        "scope_matrix",
        "handoff_chain",
        "mixed_commit",
        "qc_report_guard",
        "guard_sh",
    }
    assert set(payload["results"].keys()) == expected_keys
    assert payload["overall"] in ("PASS", "WARN", "FAIL")
    assert "verified_at" in payload
    assert "details" in payload


def test_top_level_fail_reasons_on_fail(full_setup):
    """top-level ``fail_reasons`` 가 FAIL 사유 리스트를 노출 (Codex 결함 #5).

    spec 3.x: evidence JSON 최상위에 ``fail_reasons`` 필수.
    """
    s = full_setup
    # dirty_tree FAIL 유발
    (s["worktree"] / "stray.txt").write_text("x\n", encoding="utf-8")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["overall"] == "FAIL"
    # top-level 필드 존재 + 비어있지 않음
    assert "fail_reasons" in payload
    assert isinstance(payload["fail_reasons"], list)
    assert payload["fail_reasons"], "fail_reasons 가 비어있음"
    # 호환: details.fail_reasons 동기화
    assert payload["details"]["fail_reasons"] == payload["fail_reasons"]


def test_handoff_task_id_mismatch_fail(full_setup):
    """handoff JSON 의 task_id 가 인자 task_id 와 다르면 FAIL (Codex 결함 #3)."""
    s = full_setup
    handoff = s["workspace"] / "memory" / "handoffs" / f"{s['task_id']}.json"
    handoff.parent.mkdir(parents=True, exist_ok=True)
    handoff.write_text(
        json.dumps(
            {
                "handoff_id": "h-1",
                "from_bot": "dev1",
                "to_bot": "dev5",
                "task_id": "task-9999",  # mismatch
                "timestamp": "2026-05-05T00:00:00Z",
            }
        ),
        encoding="utf-8",
    )
    _commit_all(s["workspace"], f"[{s['task_id']}] add handoff (mismatch)")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["handoff_chain"] == "FAIL"
    assert any("mismatch" in r for r in payload["fail_reasons"])


def test_handoff_from_bot_empty_fail(full_setup):
    """handoff JSON 의 from_bot 이 빈 문자열이면 FAIL (Codex 결함 #3)."""
    s = full_setup
    handoff = s["workspace"] / "memory" / "handoffs" / f"{s['task_id']}.json"
    handoff.parent.mkdir(parents=True, exist_ok=True)
    handoff.write_text(
        json.dumps(
            {
                "handoff_id": "h-1",
                "from_bot": "",
                "to_bot": "dev5",
                "task_id": s["task_id"],
                "timestamp": "2026-05-05T00:00:00Z",
            }
        ),
        encoding="utf-8",
    )
    _commit_all(s["workspace"], f"[{s['task_id']}] add handoff (empty from_bot)")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["handoff_chain"] == "FAIL"


def test_mixed_commit_detector_missing_fail(full_setup):
    """detector 부재 시 mixed_commit FAIL (안전 측 차단 — Codex 결함 #4)."""
    s = full_setup
    detector = s["workspace"] / "scripts" / "mixed_commit_detector.py"
    detector.unlink()
    _commit_all(s["workspace"], f"[{s['task_id']}] remove detector")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["mixed_commit"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1


def test_overall_priority_fail_over_warn(tmp_path):
    """FAIL 1건 + 다른 PASS 들 → overall FAIL, exit 1."""
    s = setup_full_repo(tmp_path)
    # untracked 추가 → dirty_tree FAIL
    (s["worktree"] / "extra.tmp").write_text("x\n", encoding="utf-8")
    res = run_verify(
        s["task_id"], bot=s["bot"], workspace=s["workspace"], cwd=s["worktree"]
    )
    payload = _parse_payload(res)
    assert payload["results"]["dirty_tree"] == "FAIL"
    assert payload["overall"] == "FAIL"
    assert res.returncode == 1
