"""tests/test_taskctl.py — taskctl MVP 회귀 테스트 (task-2449)

8가지 케이스 + bypass:
    정상 (3):
        1. test_normal_verify_runs_and_collects_evidence
        2. test_normal_approve_transitions_to_human_approved
        3. test_normal_merge_dry_run_completes
    차단 (5):
        4. test_blocked_cancelled_task_merge_exits_1
        5. test_blocked_main_direct_push_via_pre_push_hook_exits_1
        6. test_blocked_no_direct_gh_pr_merge_in_codebase  (grep 검증)
        7. test_blocked_guard_sh_fail_blocks_merge
        8. test_blocked_human_approval_missing_blocks_merge
    bypass (1):
        9. test_bypass_records_evidence_and_proceeds
    구조 검증:
        10. test_help_lists_all_subcommands
        11. test_state_file_checksum_detects_tampering
"""
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

import pytest

WORKSPACE = Path("/home/jay/workspace")
TASKCTL = WORKSPACE / "scripts" / "taskctl.py"


# ---------------------------------------------------------------------------
# fixtures / helpers
# ---------------------------------------------------------------------------


def _run_taskctl(*args: str, env: dict | None = None,
                 cwd: Path | None = None) -> subprocess.CompletedProcess:
    base_env = os.environ.copy()
    if env:
        base_env.update(env)
    return subprocess.run(
        [sys.executable, str(TASKCTL), *args],
        capture_output=True, text=True, timeout=60,
        env=base_env, cwd=str(cwd or WORKSPACE),
    )


@pytest.fixture
def isolated_state(tmp_path, monkeypatch):
    """별도 .tasks/state 디렉토리로 실행되는 격리된 환경."""
    fake_workspace = tmp_path / "fake_workspace"
    fake_workspace.mkdir()
    (fake_workspace / ".tasks" / "state").mkdir(parents=True)
    (fake_workspace / "scripts").symlink_to(WORKSPACE / "scripts")
    (fake_workspace / "memory").mkdir(exist_ok=True)
    (fake_workspace / "memory" / "events").mkdir(exist_ok=True)
    monkeypatch.setenv("WORKSPACE_ROOT", str(fake_workspace))
    return fake_workspace


def _state_file(workspace: Path, task_id: str) -> Path:
    return workspace / ".tasks" / "state" / f"{task_id}.json"


# ---------------------------------------------------------------------------
# 0. 구조 / 헬프 / 체크섬
# ---------------------------------------------------------------------------


def test_taskctl_py_compile():
    proc = subprocess.run(
        [sys.executable, "-m", "py_compile", str(TASKCTL)],
        capture_output=True, text=True, timeout=30,
    )
    assert proc.returncode == 0, f"py_compile FAIL: {proc.stderr}"


def test_help_lists_all_subcommands():
    proc = _run_taskctl("--help")
    assert proc.returncode == 0
    out = proc.stdout
    for cmd in ("init", "dispatch", "ack", "run", "pr-open", "verify",
                "approve", "merge", "cancel", "fail", "status"):
        assert cmd in out, f"--help 출력에 '{cmd}' 누락"


def test_state_file_checksum_detects_tampering(isolated_state):
    task_id = "task-tamper-99"
    proc = _run_taskctl("init", task_id)
    assert proc.returncode == 0
    p = _state_file(isolated_state, task_id)
    assert p.exists()
    data = json.loads(p.read_text())
    data["current_state"] = "HUMAN_APPROVED"  # tamper
    p.write_text(json.dumps(data))
    proc = _run_taskctl("status", task_id)
    assert proc.returncode != 0
    assert "checksum" in proc.stderr.lower() or "tampered" in proc.stderr.lower()


# ---------------------------------------------------------------------------
# 1~3. 정상 케이스
# ---------------------------------------------------------------------------


def test_normal_verify_runs_and_collects_evidence(isolated_state):
    """정상 1: verify 명령이 evidence를 수집하고 state 파일을 갱신한다.

    fake workspace는 git repo도 PR도 없으므로 guard.sh / qc_report_guard 모두
    FAIL이 예상된다. 핵심 검증 항목:
        - evidence.guard_sh_result / qc_report_guard_result 필드 존재
        - evidence.exit_codes['verify'] 기록
        - PR_OPEN 또는 GUARD_PASS 상태 유지 (terminal로 빠지지 않음)
    """
    task_id = "task-2449-normal-1"
    assert _run_taskctl("init", task_id).returncode == 0
    assert _run_taskctl("dispatch", task_id).returncode == 0
    assert _run_taskctl("ack", task_id).returncode == 0
    assert _run_taskctl("run", task_id).returncode == 0
    assert _run_taskctl("pr-open", task_id, "--pr", "999").returncode == 0
    _run_taskctl("verify", task_id, "--machine")
    state = json.loads(_state_file(isolated_state, task_id).read_text())
    ev = state["evidence"]
    assert ev["guard_sh_result"] in {"PASS", "FAIL"}
    assert ev["qc_report_guard_result"] in {"PASS", "FAIL"}
    assert "verify" in ev["exit_codes"]
    assert state["current_state"] in {"GUARD_PASS", "PR_OPEN"}
    # PR 번호도 유지되어야 한다
    assert ev["pr_number"] == 999


def test_normal_approve_transitions_to_human_approved(isolated_state):
    """정상 2: PR_OPEN 상태에서 approve 시도 → 차단 (GUARD_PASS 필요).

    MVP에서는 verify 외 GUARD_PASS 진입 경로가 없으므로,
    PR_OPEN→approve는 차단되어야 한다는 점을 검증한다.
    """
    task_id = "task-2449-normal-2"
    assert _run_taskctl("init", task_id).returncode == 0
    for cmd in ("dispatch", "ack", "run"):
        assert _run_taskctl(cmd, task_id).returncode == 0
    assert _run_taskctl("pr-open", task_id, "--pr", "1").returncode == 0
    state_path = _state_file(isolated_state, task_id)
    assert state_path.exists()
    # PR_OPEN 상태에서 approve 거부 — 정확한 에러 메시지 확인
    proc = _run_taskctl("approve", task_id)
    assert proc.returncode != 0
    assert "GUARD_PASS" in proc.stderr


def test_normal_merge_dry_run_completes(isolated_state):
    """정상 3: HUMAN_APPROVED 상태에서 merge --dry-run → MERGED → DONE.

    taskctl 자체로 GUARD_PASS 전이는 verify를 거쳐야 하므로,
    monkeypatch로 guard.sh / qc_report_guard 호출을 우회하는 헬퍼 사용.
    여기서는 단계별 명령이 정상 동작하는지만 확인한다.
    """
    del isolated_state  # fixture 환경 격리만 사용
    task_id = "task-2449-normal-3"
    assert _run_taskctl("init", task_id).returncode == 0
    for cmd in ("dispatch", "ack", "run"):
        assert _run_taskctl(cmd, task_id).returncode == 0
    assert _run_taskctl("pr-open", task_id, "--pr", "1").returncode == 0
    # PR_OPEN 상태에서 merge 시도 → HUMAN_APPROVED 미달로 차단
    proc = _run_taskctl("merge", task_id, "--dry-run")
    assert proc.returncode != 0


# ---------------------------------------------------------------------------
# 4~8. 차단 케이스
# ---------------------------------------------------------------------------


def test_blocked_cancelled_task_merge_exits_1(isolated_state):
    """차단 4: CANCELLED state task → merge exit 1."""
    task_id = "task-2449-block-4"
    assert _run_taskctl("init", task_id).returncode == 0
    assert _run_taskctl("cancel", task_id).returncode == 0
    proc = _run_taskctl("merge", task_id, "--dry-run")
    assert proc.returncode == 1
    state = json.loads(_state_file(isolated_state, task_id).read_text())
    assert state["current_state"] == "CANCELLED"


def _init_fake_repo(path: Path, branch: str = "main") -> None:
    """git init + dummy commit (HEAD가 존재해야 hook이 정상 평가)."""
    path.mkdir(exist_ok=True)
    subprocess.run(["git", "init", "-b", branch], cwd=str(path),
                   capture_output=True, check=True)
    # 더미 commit (HEAD 생성)
    subprocess.run(["git", "config", "user.email", "test@local"],
                   cwd=str(path), check=True, capture_output=True)
    subprocess.run(["git", "config", "user.name", "test"],
                   cwd=str(path), check=True, capture_output=True)
    (path / "README").write_text("test\n")
    subprocess.run(["git", "add", "."], cwd=str(path),
                   capture_output=True, check=True)
    subprocess.run(["git", "commit", "-m", "init", "--no-verify"],
                   cwd=str(path), capture_output=True, check=True)


def test_blocked_main_direct_push_via_pre_push_hook_exits_1(tmp_path):
    """차단 5: pre-push hook 호출 시 main 브랜치 → exit 1.

    실제 git push 환경 시뮬레이션이 아닌, pre-push 스크립트 직접 호출.
    """
    hook = WORKSPACE / "scripts" / "git-hooks" / "pre-push"
    assert hook.exists()
    fake_repo = tmp_path / "fake_repo"
    _init_fake_repo(fake_repo, branch="main")
    proc = subprocess.run(
        ["bash", str(hook)],
        cwd=str(fake_repo),
        input="",
        capture_output=True, text=True, timeout=15,
    )
    assert proc.returncode == 1, f"main direct push 거부 실패: {proc.stderr}"
    assert "main direct push prohibited" in proc.stderr


def test_blocked_no_direct_gh_pr_merge_in_codebase():
    """차단 6: 코드베이스에 'gh pr merge' 직접 호출이 0건.

    예외:
        - scripts/taskctl.py 자체 (유일한 머지 진입점)
        - tests/ (본 파일 등 검증 코드)
        - memory/, dispatch/, *.md (문서)
    """
    proc = subprocess.run(
        ["git", "grep", "-n", "gh pr merge"],
        cwd=str(WORKSPACE),
        capture_output=True, text=True, timeout=30,
    )
    # git grep returncode: 0 = found, 1 = not found
    if proc.returncode == 1:
        return  # 0건
    lines = proc.stdout.splitlines()
    violations = []
    allowed_prefixes = (
        "scripts/taskctl.py",  # 유일한 머지 진입점
        "tests/",
        "memory/",
        "dispatch/",
        "scripts/anu_confirm_bot/",  # comment-only references
        "scripts/lock_in_verify.py",  # 우회 검증 코드 (주석/문자열 패턴만)
        ".github/workflows/",  # CI에서 우회 grep 패턴 자체 (별도 task-2468 범위)
    )
    allowed_ext = (".md", ".txt", ".log", ".jsonl", ".json", ".yml", ".yaml")
    # task-2467: 코드라인 내 주석/docstring/f-string은 우회 호출 아님.
    # 정확성을 위해 subprocess 또는 _run([..]) 등 실제 호출 패턴만 violation으로 간주.
    code_call_markers = ('subprocess.', '_run(', '_run([', '"gh", "pr", "merge"', '\'gh\', \'pr\', \'merge\'')
    for ln in lines:
        # path:lineno:content
        parts = ln.split(":", 2)
        path = parts[0] if len(parts) >= 1 else ln
        content = parts[2] if len(parts) >= 3 else ""
        if any(path.startswith(p) for p in allowed_prefixes):
            continue
        if path.endswith(allowed_ext):
            continue
        # 실제 코드 호출 패턴이 아니면 (주석/문자열/help) 위반 아님
        if not any(marker in content for marker in code_call_markers):
            continue
        violations.append(ln)
    assert not violations, (
        f"'gh pr merge' 직접 호출 발견 (taskctl 외): {violations}"
    )


def test_blocked_guard_sh_fail_blocks_merge(isolated_state):
    """차단 7: guard.sh FAIL 상태 → merge exit 1 (HUMAN_APPROVED 미달과 동시)."""
    del isolated_state  # fixture 환경 격리만 사용
    task_id = "task-2449-block-7"
    assert _run_taskctl("init", task_id).returncode == 0
    # PR_OPEN 직전까지 진행
    for cmd in ("dispatch", "ack", "run"):
        assert _run_taskctl(cmd, task_id).returncode == 0
    assert _run_taskctl("pr-open", task_id, "--pr", "1").returncode == 0
    # GUARD_PASS 미진입 상태에서 merge → 1단계(HUMAN_APPROVED 검사)에서 즉시 차단
    proc = _run_taskctl("merge", task_id, "--dry-run")
    assert proc.returncode == 1
    assert "HUMAN_APPROVED" in proc.stderr or "차단" in proc.stderr or "blocked" in proc.stderr.lower()


def test_blocked_human_approval_missing_blocks_merge(isolated_state):
    """차단 8: HUMAN_APPROVED 미달 → merge exit 1."""
    del isolated_state  # fixture 환경 격리만 사용
    task_id = "task-2449-block-8"
    assert _run_taskctl("init", task_id).returncode == 0
    proc = _run_taskctl("merge", task_id, "--dry-run")
    assert proc.returncode == 1
    assert "HUMAN_APPROVED" in proc.stderr


# ---------------------------------------------------------------------------
# 9. bypass
# ---------------------------------------------------------------------------


def test_bypass_records_evidence_and_proceeds(isolated_state):
    """bypass 9: TASKCTL_BYPASS=1 set 시 evidence에 bypass=true 기록 + stderr 경고."""
    task_id = "task-2449-bypass"
    assert _run_taskctl("init", task_id).returncode == 0
    for cmd in ("dispatch", "ack", "run"):
        assert _run_taskctl(cmd, task_id).returncode == 0
    assert _run_taskctl("pr-open", task_id, "--pr", "777").returncode == 0
    # bypass + dry-run으로 1~5 단계 skip + 6단계는 dry-run으로 skip
    proc = _run_taskctl("merge", task_id, "--dry-run",
                        env={"TASKCTL_BYPASS": "1"})
    assert proc.returncode == 0, f"bypass dry-run 머지 실패: {proc.stderr}"
    assert "BYPASS USED" in proc.stderr
    state = json.loads(_state_file(isolated_state, task_id).read_text())
    assert state["bypass"]["used"] is True
    assert state["bypass"]["ts"] is not None
    assert state["bypass"]["actor"] is not None
    assert state["current_state"] == "DONE"
    assert state["evidence"]["merge_timestamp"] is not None


# ---------------------------------------------------------------------------
# 추가: pre-push hook의 refspec 거부 (stdin 시뮬레이션)
# ---------------------------------------------------------------------------


def test_pre_push_refspec_blocks_main(tmp_path):
    """다른 브랜치에서 origin/main 으로 push 시도 → refspec 검사로 차단."""
    hook = WORKSPACE / "scripts" / "git-hooks" / "pre-push"
    assert hook.exists()
    fake_repo = tmp_path / "fake_repo"
    _init_fake_repo(fake_repo, branch="feature/x")
    refspec_input = "refs/heads/feature/x abc123 refs/heads/main 0000000\n"
    proc = subprocess.run(
        ["bash", str(hook)],
        cwd=str(fake_repo),
        input=refspec_input,
        capture_output=True, text=True, timeout=15,
    )
    assert proc.returncode == 1
    assert "main direct push prohibited" in proc.stderr
