"""tests/regression/test_taskctl_reconcile.py — task-2472+1 회귀 테스트 (T1~T8).

헤임달 (개발2팀 테스트 엔지니어) 작성.
reconcile_orphaned_merge 공개 API를 격리된 환경에서 검증한다.

주의:
- 외부 gh/git 호출 금지. fake_gh 또는 monkeypatch로 격리.
- state_dir_override, events_dir_override, audit_path_override로 tmp_path 격리.
- forbidden_paths(memory/events/task-2472.* 등) 절대 변경 금지.
"""
from __future__ import annotations

import hashlib
import json
import os
import sys
from pathlib import Path

# worktree root를 sys.path에 등록 (import 충돌 방지)
_WT_ROOT = Path(__file__).resolve().parents[2]
if str(_WT_ROOT) not in sys.path:
    sys.path.insert(0, str(_WT_ROOT))

from utils.state_repair import (  # type: ignore[import]  # noqa: E402
    RECONCILE_AUDIT_FIELDS,
    compute_done_sha256,
    reconcile_orphaned_merge,
)


# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------


def make_fake_gh(tmp_path: Path, payload: dict) -> list[str]:
    """tmp_path에 fake gh shell script 생성. payload를 JSON으로 echo한다."""
    gh_dir = tmp_path / "fake_gh_bin"
    gh_dir.mkdir(exist_ok=True)
    script = gh_dir / "gh"
    script.write_text(
        f'#!/usr/bin/env bash\necho \'{json.dumps(payload)}\'\n'
    )
    script.chmod(0o755)
    return [str(script)]


def make_done_file(events_dir: Path, task_id: str, qc_result: str = "PASS") -> Path:
    """task-timer 포맷 .done 파일 생성."""
    done_path = events_dir / f"{task_id}.done"
    payload = {
        "task_id": task_id,
        "qc_result": qc_result,
        "completed_at": "2026-05-07T00:00:00Z",
    }
    done_path.write_text(json.dumps(payload), encoding="utf-8")
    return done_path


def write_audit_line(audit_path: Path, task_id: str, done_sha256: str) -> None:
    """audit jsonl에 done_sha256 포함 최소 라인 작성 (사전 시드)."""
    audit_path.parent.mkdir(parents=True, exist_ok=True)
    record = {
        "task_id": task_id,
        "ts": "2026-05-01T00:00:00Z",
        "classification": "state_orphaned_after_valid_merge",
        "pr": 40,
        "merge_commit": "abc1234abc1234",
        "origin_main_head": "",
        "ancestry": "PASS",
        "done_path": f"memory/events/{task_id}.done",
        "done_sha256": done_sha256,
        "g3_fail_classification": "no_g3_fail",
        "actor": "human <human@test>",
        "evidence_path": "memory/orchestration-audit/state-recovery.jsonl",
        "approved_by_chairman": True,
        "decision": "pre_seed",
    }
    with open(str(audit_path), "a", encoding="utf-8") as f:
        f.write(json.dumps(record) + "\n")


def assert_all_audit_fields(workspace: Path, task_id: str) -> dict:
    """workspace/memory/orchestration-audit/state-recovery.jsonl에서
    task_id 매칭 reconcile_state_created 레코드 파싱 + 14필드 검증.

    append_reconcile_audit는 workspace 기반으로 고정 경로에 기록하므로
    audit_path_override가 아닌 workspace 경로를 사용한다.
    """
    audit_path = workspace / "memory" / "orchestration-audit" / "state-recovery.jsonl"
    assert audit_path.exists(), f"audit jsonl 없음: {audit_path}"
    lines = audit_path.read_text(encoding="utf-8").strip().splitlines()
    record = None
    for line in reversed(lines):
        line = line.strip()
        if not line:
            continue
        obj = json.loads(line)
        if obj.get("task_id") == task_id and obj.get("decision") == "reconcile_state_created":
            record = obj
            break
    assert record is not None, f"audit에서 task_id={task_id} reconcile_state_created 라인 없음"
    missing = [f for f in RECONCILE_AUDIT_FIELDS if f not in record]
    assert not missing, f"audit 14필드 누락: {missing}"
    for field in RECONCILE_AUDIT_FIELDS:
        assert record[field] is not None, f"audit 필드 {field!r} 값이 None"
    return record


# ---------------------------------------------------------------------------
# T1: state file missing + PR merged + valid .done → reconcile 성공
# ---------------------------------------------------------------------------


def test_t1_reconcile_success(tmp_path: Path, monkeypatch):
    """T1: state file missing + PR merged + valid .done → reconcile 성공."""
    task_id = "task-t1-reconcile"
    pr_number = 100
    merge_commit_sha = "abc1234abc1234abc1234abc1234abc1234abc1234"

    # fake gh — MERGED 상태
    gh_payload = {
        "state": "MERGED",
        "mergedAt": "2026-05-01T12:00:00Z",
        "mergeCommit": {"oid": merge_commit_sha},
    }
    fake_gh = make_fake_gh(tmp_path, gh_payload)

    # events_dir에 .done 생성
    events_dir = tmp_path / "events"
    events_dir.mkdir()
    done_path = make_done_file(events_dir, task_id)
    done_sha = compute_done_sha256(done_path)

    # audit 사전 시드
    audit_path = tmp_path / "audit" / "state-recovery.jsonl"
    write_audit_line(audit_path, task_id, done_sha)

    # state_dir (비어 있음 — state file missing)
    state_dir = tmp_path / "state"

    # silent_corruption_guard.check_origin_main_ancestry monkeypatch
    monkeypatch.setattr(
        "utils.state_repair.sys.modules",
        dict(sys.modules),
    )
    import utils.silent_corruption_guard as scg_mod  # type: ignore[import]
    monkeypatch.setattr(
        scg_mod,
        "check_origin_main_ancestry",
        lambda sha, cwd=None: {
            "ok": True,
            "reason": "ancestry PASS (monkeypatched)",
            "detail": {"merge_commit_sha": sha, "origin_sha": "origin_head_fake"},
        },
    )

    result = reconcile_orphaned_merge(
        task_id=task_id,
        pr_number=pr_number,
        merge_commit_sha=merge_commit_sha,
        evidence_path=str(audit_path),
        repo="test/repo",
        workspace=tmp_path,
        approved_by_chairman=True,
        gh_cmd=fake_gh,
        state_dir_override=state_dir,
        events_dir_override=events_dir,
        audit_path_override=audit_path,
    )

    assert result["ok"] is True, f"reconcile 실패: {result['reason']}"

    # state file 생성 + current_state == DONE
    state_file = state_dir / f"{task_id}.json"
    assert state_file.exists(), "state 파일 미생성"
    state_data = json.loads(state_file.read_text())
    assert state_data["current_state"] == "DONE"

    # _checksum 유효성 (재계산)
    stored_checksum = state_data.pop("_checksum")
    canon = json.dumps(state_data, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
    expected_checksum = hashlib.sha256(canon.encode("utf-8")).hexdigest()
    assert stored_checksum == expected_checksum, "state checksum 불일치"

    # audit 14필드 append 검증 (workspace 기반 경로 사용)
    assert_all_audit_fields(tmp_path, task_id)


# ---------------------------------------------------------------------------
# T2: PR not merged → reject (step 1 fail)
# ---------------------------------------------------------------------------


def test_t2_pr_not_merged(tmp_path: Path, monkeypatch):
    """T2: PR not merged → step 1 실패."""
    task_id = "task-t2-open-pr"
    pr_number = 200

    gh_payload = {
        "state": "OPEN",
        "mergedAt": None,
        "mergeCommit": None,
    }
    fake_gh = make_fake_gh(tmp_path, gh_payload)

    events_dir = tmp_path / "events"
    events_dir.mkdir()
    state_dir = tmp_path / "state"
    audit_path = tmp_path / "audit" / "state-recovery.jsonl"

    result = reconcile_orphaned_merge(
        task_id=task_id,
        pr_number=pr_number,
        merge_commit_sha="deadbeef" * 5,
        evidence_path=str(audit_path),
        repo="test/repo",
        workspace=tmp_path,
        approved_by_chairman=True,
        gh_cmd=fake_gh,
        state_dir_override=state_dir,
        events_dir_override=events_dir,
        audit_path_override=audit_path,
    )

    assert result["ok"] is False
    assert result["step_failed"] == 1, f"step_failed={result['step_failed']} (1 예상)"


# ---------------------------------------------------------------------------
# T3: invalid .done (스키마 미준수) → reject
# ---------------------------------------------------------------------------


def test_t3_invalid_done_schema(tmp_path: Path, monkeypatch):
    """T3: invalid .done (스키마 미준수) → schema 오류로 reject."""
    task_id = "task-t3-bad-done"
    pr_number = 300
    merge_commit_sha = "ccccddddeeeeffffccccddddeeeeffffccccdddd"

    gh_payload = {
        "state": "MERGED",
        "mergedAt": "2026-05-02T10:00:00Z",
        "mergeCommit": {"oid": merge_commit_sha},
    }
    fake_gh = make_fake_gh(tmp_path, gh_payload)

    events_dir = tmp_path / "events"
    events_dir.mkdir()

    # 잘못된 .done — 필수 필드 없음
    done_path = events_dir / f"{task_id}.done"
    done_path.write_text(json.dumps({"foo": "bar"}), encoding="utf-8")
    done_sha = compute_done_sha256(done_path)

    audit_path = tmp_path / "audit" / "state-recovery.jsonl"
    write_audit_line(audit_path, task_id, done_sha)

    state_dir = tmp_path / "state"

    import utils.silent_corruption_guard as scg_mod  # type: ignore[import]
    monkeypatch.setattr(
        scg_mod,
        "check_origin_main_ancestry",
        lambda sha, cwd=None: {
            "ok": True,
            "reason": "ancestry PASS",
            "detail": {"merge_commit_sha": sha, "origin_sha": "fake_origin"},
        },
    )

    result = reconcile_orphaned_merge(
        task_id=task_id,
        pr_number=pr_number,
        merge_commit_sha=merge_commit_sha,
        evidence_path=str(audit_path),
        repo="test/repo",
        workspace=tmp_path,
        approved_by_chairman=True,
        gh_cmd=fake_gh,
        state_dir_override=state_dir,
        events_dir_override=events_dir,
        audit_path_override=audit_path,
    )

    assert result["ok"] is False
    # step 3 또는 reason에 "schema" 포함
    assert result["step_failed"] == 3 or "schema" in result["reason"].lower() or ".done" in result["reason"], (
        f"예상과 다른 실패: step={result['step_failed']}, reason={result['reason']}"
    )


# ---------------------------------------------------------------------------
# T4: .g3-fail unresolved → reject
# ---------------------------------------------------------------------------


def test_t4_g3_fail_unresolved(tmp_path: Path, monkeypatch):
    """T4: .g3-fail이 unresolved (fail_reasons에 'test failure') → reconcile 차단."""
    task_id = "task-t4-g3-unresolved"
    pr_number = 400
    merge_commit_sha = "dddd1111dddd2222dddd3333dddd4444dddd5555"

    gh_payload = {
        "state": "MERGED",
        "mergedAt": "2026-05-03T10:00:00Z",
        "mergeCommit": {"oid": merge_commit_sha},
    }
    fake_gh = make_fake_gh(tmp_path, gh_payload)

    events_dir = tmp_path / "events"
    events_dir.mkdir()

    # 정상 .done
    done_path = make_done_file(events_dir, task_id)
    done_sha = compute_done_sha256(done_path)

    # .g3-fail — unresolved 패턴 (fail_reasons에 "test failure")
    g3_fail_path = events_dir / f"{task_id}.g3-fail"
    g3_fail_path.write_text(
        json.dumps({"fail_reasons": ["test failure: pytest exit code 1"]}),
        encoding="utf-8",
    )

    audit_path = tmp_path / "audit" / "state-recovery.jsonl"
    write_audit_line(audit_path, task_id, done_sha)

    state_dir = tmp_path / "state"

    import utils.silent_corruption_guard as scg_mod  # type: ignore[import]
    monkeypatch.setattr(
        scg_mod,
        "check_origin_main_ancestry",
        lambda sha, cwd=None: {
            "ok": True,
            "reason": "ancestry PASS",
            "detail": {"merge_commit_sha": sha, "origin_sha": "fake_origin"},
        },
    )

    result = reconcile_orphaned_merge(
        task_id=task_id,
        pr_number=pr_number,
        merge_commit_sha=merge_commit_sha,
        evidence_path=str(audit_path),
        repo="test/repo",
        workspace=tmp_path,
        approved_by_chairman=True,
        gh_cmd=fake_gh,
        state_dir_override=state_dir,
        events_dir_override=events_dir,
        audit_path_override=audit_path,
    )

    assert result["ok"] is False
    reason_lower = result["reason"].lower()
    assert "g3" in reason_lower or "unresolved" in reason_lower, (
        f"reason에 'g3' 또는 'unresolved' 없음: {result['reason']}"
    )


# ---------------------------------------------------------------------------
# T5: mergeCommit not in origin/main → reject (step 2 fail)
# ---------------------------------------------------------------------------


def test_t5_ancestry_fail(tmp_path: Path, monkeypatch):
    """T5: PR=MERGED 이지만 ancestry 검증 실패 → step 2 실패."""
    task_id = "task-t5-ancestry-fail"
    pr_number = 500
    merge_commit_sha = "eeee1111eeee2222eeee3333eeee4444eeee5555"

    gh_payload = {
        "state": "MERGED",
        "mergedAt": "2026-05-04T10:00:00Z",
        "mergeCommit": {"oid": merge_commit_sha},
    }
    fake_gh = make_fake_gh(tmp_path, gh_payload)

    events_dir = tmp_path / "events"
    events_dir.mkdir()
    state_dir = tmp_path / "state"
    audit_path = tmp_path / "audit" / "state-recovery.jsonl"

    # ancestry monkeypatch → ok=False
    import utils.silent_corruption_guard as scg_mod  # type: ignore[import]
    monkeypatch.setattr(
        scg_mod,
        "check_origin_main_ancestry",
        lambda sha, cwd=None: {
            "ok": False,
            "reason": "merge_commit not ancestor of origin/main (monkeypatched)",
            "detail": {"merge_commit_sha": sha, "origin_sha": "fake_origin"},
        },
    )

    result = reconcile_orphaned_merge(
        task_id=task_id,
        pr_number=pr_number,
        merge_commit_sha=merge_commit_sha,
        evidence_path=str(audit_path),
        repo="test/repo",
        workspace=tmp_path,
        approved_by_chairman=True,
        gh_cmd=fake_gh,
        state_dir_override=state_dir,
        events_dir_override=events_dir,
        audit_path_override=audit_path,
    )

    assert result["ok"] is False
    assert result["step_failed"] == 2, f"step_failed={result['step_failed']} (2 예상)"


# ---------------------------------------------------------------------------
# T6: checksum mismatch → reject
# ---------------------------------------------------------------------------


def test_t6_checksum_mismatch(tmp_path: Path, monkeypatch):
    """T6: audit의 done_sha256과 실제 .done sha256 불일치 → reject."""
    task_id = "task-t6-sha256-mismatch"
    pr_number = 600
    merge_commit_sha = "ffff1111ffff2222ffff3333ffff4444ffff5555"

    gh_payload = {
        "state": "MERGED",
        "mergedAt": "2026-05-05T10:00:00Z",
        "mergeCommit": {"oid": merge_commit_sha},
    }
    fake_gh = make_fake_gh(tmp_path, gh_payload)

    events_dir = tmp_path / "events"
    events_dir.mkdir()

    done_path = make_done_file(events_dir, task_id)
    # 의도적으로 다른 sha256을 audit에 기록
    wrong_sha = "a" * 64

    audit_path = tmp_path / "audit" / "state-recovery.jsonl"
    write_audit_line(audit_path, task_id, wrong_sha)

    state_dir = tmp_path / "state"

    import utils.silent_corruption_guard as scg_mod  # type: ignore[import]
    monkeypatch.setattr(
        scg_mod,
        "check_origin_main_ancestry",
        lambda sha, cwd=None: {
            "ok": True,
            "reason": "ancestry PASS",
            "detail": {"merge_commit_sha": sha, "origin_sha": "fake_origin"},
        },
    )

    result = reconcile_orphaned_merge(
        task_id=task_id,
        pr_number=pr_number,
        merge_commit_sha=merge_commit_sha,
        evidence_path=str(audit_path),
        repo="test/repo",
        workspace=tmp_path,
        approved_by_chairman=True,
        gh_cmd=fake_gh,
        state_dir_override=state_dir,
        events_dir_override=events_dir,
        audit_path_override=audit_path,
    )

    assert result["ok"] is False
    reason_lower = result["reason"].lower()
    assert "sha256" in reason_lower or "checksum" in reason_lower, (
        f"reason에 'sha256' 또는 'checksum' 없음: {result['reason']}"
    )


# ---------------------------------------------------------------------------
# T7: reconcile audit 14필드 검증
# ---------------------------------------------------------------------------


def test_t7_audit_14_fields(tmp_path: Path, monkeypatch):
    """T7: 성공 시나리오에서 audit jsonl 14필드 모두 존재 + None 아님."""
    task_id = "task-t7-audit-fields"
    pr_number = 700
    merge_commit_sha = "aaaa1111bbbb2222cccc3333dddd4444eeee5555"

    gh_payload = {
        "state": "MERGED",
        "mergedAt": "2026-05-06T10:00:00Z",
        "mergeCommit": {"oid": merge_commit_sha},
    }
    fake_gh = make_fake_gh(tmp_path, gh_payload)

    events_dir = tmp_path / "events"
    events_dir.mkdir()
    done_path = make_done_file(events_dir, task_id)
    done_sha = compute_done_sha256(done_path)

    audit_path = tmp_path / "audit" / "state-recovery.jsonl"
    write_audit_line(audit_path, task_id, done_sha)

    state_dir = tmp_path / "state"

    import utils.silent_corruption_guard as scg_mod  # type: ignore[import]
    monkeypatch.setattr(
        scg_mod,
        "check_origin_main_ancestry",
        lambda sha, cwd=None: {
            "ok": True,
            "reason": "ancestry PASS",
            "detail": {"merge_commit_sha": sha, "origin_sha": "t7_origin_head"},
        },
    )

    result = reconcile_orphaned_merge(
        task_id=task_id,
        pr_number=pr_number,
        merge_commit_sha=merge_commit_sha,
        evidence_path=str(audit_path),
        repo="test/repo",
        workspace=tmp_path,
        approved_by_chairman=True,
        gh_cmd=fake_gh,
        state_dir_override=state_dir,
        events_dir_override=events_dir,
        audit_path_override=audit_path,
    )

    assert result["ok"] is True, f"reconcile 실패: {result['reason']}"

    # audit 14필드 전체 검증 (workspace 기반 경로)
    record = assert_all_audit_fields(tmp_path, task_id)

    # 14필드 명시적으로 확인
    for field in RECONCILE_AUDIT_FIELDS:
        assert field in record, f"필드 {field!r} 없음"
        assert record[field] is not None, f"필드 {field!r} 값이 None"


# ---------------------------------------------------------------------------
# T8: dogfooding (task-2472 격리 시뮬레이션)
# ---------------------------------------------------------------------------


def test_t8_dogfooding_simulation(tmp_path: Path, monkeypatch):
    """T8: task-2472 케이스 격리 재현. 실제 파일 변경 없이 unit test로 검증.

    task_id="task-2472-dogfooding-test" 로 격리.
    """
    task_id = "task-2472-dogfooding-test"
    pr_number = 40
    merge_commit_sha = "6ec4e0d8a4e4d2dd39fbd13eb19a69d4212faeba"

    gh_payload = {
        "state": "MERGED",
        "mergedAt": "2026-04-20T08:00:00Z",
        "mergeCommit": {"oid": merge_commit_sha},
    }
    fake_gh = make_fake_gh(tmp_path, gh_payload)

    events_dir = tmp_path / "events"
    events_dir.mkdir()

    # task-2472 패턴: qc_result=PASS .done
    done_path = events_dir / f"{task_id}.done"
    done_path.write_text(
        json.dumps({
            "task_id": task_id,
            "qc_result": "PASS",
            "completed_at": "2026-04-20T07:55:00Z",
            "merge_commit_sha": merge_commit_sha,
        }),
        encoding="utf-8",
    )
    done_sha = compute_done_sha256(done_path)

    # .g3-fail false_alert 패턴: fail_reasons에 "report not found"만
    g3_fail_path = events_dir / f"{task_id}.g3-fail"
    g3_fail_path.write_text(
        json.dumps({"fail_reasons": ["report not found"]}),
        encoding="utf-8",
    )

    # report 파일을 .g3-fail 보다 이후 mtime으로 생성
    reports_dir = tmp_path / "memory" / "reports"
    reports_dir.mkdir(parents=True)
    report_path = reports_dir / f"{task_id}.md"
    report_path.write_text(f"# {task_id} report\n", encoding="utf-8")

    # mtime 조작: g3_fail < report
    g3_mtime = g3_fail_path.stat().st_mtime
    os.utime(str(report_path), (g3_mtime + 10, g3_mtime + 10))

    audit_path = tmp_path / "audit" / "state-recovery.jsonl"
    write_audit_line(audit_path, task_id, done_sha)

    state_dir = tmp_path / "state"

    import utils.silent_corruption_guard as scg_mod  # type: ignore[import]
    monkeypatch.setattr(
        scg_mod,
        "check_origin_main_ancestry",
        lambda sha, cwd=None: {
            "ok": True,
            "reason": "ancestry PASS (dogfooding sim)",
            "detail": {"merge_commit_sha": sha, "origin_sha": "dogfooding_origin"},
        },
    )

    # g3_fail_classifier의 report_path가 올바른 위치를 사용하도록
    # reconcile_orphaned_merge는 ws/memory/reports/{task_id}.md 를 사용
    # workspace=tmp_path 이므로 report는 tmp_path/memory/reports/{task_id}.md
    result = reconcile_orphaned_merge(
        task_id=task_id,
        pr_number=pr_number,
        merge_commit_sha=merge_commit_sha,
        evidence_path=str(audit_path),
        repo="test/repo",
        workspace=tmp_path,
        approved_by_chairman=True,
        gh_cmd=fake_gh,
        state_dir_override=state_dir,
        events_dir_override=events_dir,
        audit_path_override=audit_path,
    )

    assert result["ok"] is True, f"dogfooding 시뮬레이션 실패: {result['reason']}"

    # state 파일 검증
    state_file = state_dir / f"{task_id}.json"
    assert state_file.exists()
    state_data = json.loads(state_file.read_text())
    assert state_data["current_state"] == "DONE"

    # checksum 검증
    stored_checksum = state_data.pop("_checksum")
    canon = json.dumps(state_data, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
    expected_checksum = hashlib.sha256(canon.encode("utf-8")).hexdigest()
    assert stored_checksum == expected_checksum, "dogfooding state checksum 불일치"
