"""
test_lifecycle_guards.py — task-2468 P1 통합 테스트 14건 (A~N).

회장 명시: 14건 중 1건이라도 FAIL → ESCALATED.
task-2467+3 silent corruption 4대 결함이 본 task 코드로 차단됨을 증명.
"""
from __future__ import annotations
import json, sys
from pathlib import Path
import pytest

# scripts/ 디렉토리 sys.path 추가 (lifecycle_guards / gemini_severity_parser import용)
_REPO_ROOT = Path(__file__).resolve().parents[2]
_SCRIPTS_DIR = _REPO_ROOT / "scripts"
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))

import lifecycle_guards as lg  # noqa: E402
import gemini_severity_parser as gsp  # noqa: E402


@pytest.fixture
def tmp_events(tmp_path: Path) -> Path:
    d = tmp_path / "events"
    d.mkdir()
    return d


@pytest.fixture
def tmp_evidence(tmp_path: Path) -> Path:
    d = tmp_path / "evidence"
    d.mkdir()
    return d


# ── A. .g3-fail 존재 시 .done 생성 차단 ──
def test_A_g3_fail_blocks_done(tmp_events: Path):
    task_id = "task-test-A"
    (tmp_events / f"{task_id}.g3-fail").write_text(
        json.dumps({"task_id": task_id, "fail_reasons": ["test"]}), encoding="utf-8"
    )
    r = lg.check_g3_fail_blocks_done(task_id, events_dir=tmp_events)
    assert r.ok is False, f"expected FAIL but got {r.as_dict()}"
    assert ".g3-fail" in r.reason or "g3" in r.reason.lower()
    assert any(".g3-fail" in str(p) or ".g3_fail" in str(p) for p in r.blocking)


# ── B. .done + .g3-fail 동시 존재 → conflict ──
def test_B_done_fail_conflict(tmp_events: Path):
    task_id = "task-test-B"
    (tmp_events / f"{task_id}.g3-fail").write_text("{}", encoding="utf-8")
    (tmp_events / f"{task_id}.done").write_text("{}", encoding="utf-8")
    r = lg.check_done_fail_conflict(task_id, events_dir=tmp_events)
    assert r.ok is False
    assert "conflict" in r.reason.lower() or "동시" in r.reason


# ── C. Gemini High 3건 fixture (task-2467+3 재현) ──
def test_C_gemini_high_3_count():
    fixture = """## High Priority Issues

\U0001f534 **High:** Bot author allowlist에 `app/` prefix 누락

### Critical: self-approve guard 우회 가능

severity: critical — chairman PAT fallback 시 PR author=human

## Medium

⚠️ Medium: TASKCTL_CWD env hook은 임시방편

## Low

- Minor: typo
"""
    result = gsp.count_severities(fixture)
    assert result["high"] >= 3, f"high={result['high']} (expected >=3) — task-2467+3 미재현"


# ── D. Gemini High ≥ 1건 → auto-merge 차단 ──
def test_D_gemini_high_blocks_automerge():
    text = "## High\n\U0001f534 BLOCKING issue"
    r = lg.check_gemini_severity(text=text)
    assert r.ok is False, f"expected FAIL: {r.as_dict()}"
    # high count >=1
    assert r.detail.get("high", 0) >= 1


# ── E. approver=JonghyukJeon → manual ──
def test_E_chairman_approver_is_manual(tmp_path: Path):
    cfg = tmp_path / "allowed_approvers.json"
    cfg.write_text(json.dumps({
        "approvers": [{"login": "taskctl-gate", "type": "system"}],
        "manual_logins": ["JonghyukJeon"]
    }), encoding="utf-8")
    r = lg.check_approver_identity("JonghyukJeon", allowlist_path=cfg)
    assert r.ok is False, f"chairman approver는 manual로 분류돼야 함: {r.as_dict()}"
    assert "manual" in r.reason.lower() or "system" in r.reason.lower()


# ── F. approver=taskctl-gate → auto-approve evidence ──
def test_F_system_approver_is_auto(tmp_path: Path):
    cfg = tmp_path / "allowed_approvers.json"
    cfg.write_text(json.dumps({
        "approvers": [{"login": "taskctl-gate", "type": "system"}],
        "manual_logins": ["JonghyukJeon"]
    }), encoding="utf-8")
    r = lg.check_approver_identity("taskctl-gate", allowlist_path=cfg)
    assert r.ok is True, f"system approver: {r.as_dict()}"


# ── G. merge_commit_sha=null/empty → done 차단 ──
def test_G_merge_sha_empty_blocks(monkeypatch):
    """merge_commit_sha 빈 값 시 차단. fetch_pr_merge_sha를 mock."""
    def fake_fetch(_pr_number: int, _repo: str) -> dict:
        return {"merge_commit_sha": "", "base_ref": "main", "ok": True, "raw": {}}
    monkeypatch.setattr(lg, "fetch_pr_merge_sha", fake_fetch)
    r = lg.check_merge_commit_sha(33, repo="dummy/repo")
    assert r.ok is False
    assert "sha" in r.reason.lower() or "empty" in r.reason.lower() or "null" in r.reason.lower()


# ── H. origin/<base> HEAD ≠ merge_commit_sha → 차단 ──
def test_H_merge_sha_mismatch_blocks(monkeypatch):
    monkeypatch.setattr(lg, "fetch_pr_merge_sha",
                        lambda _pr, _repo: {"merge_commit_sha": "abc123", "base_ref": "main", "ok": True, "raw": {}})
    monkeypatch.setattr(lg, "fetch_origin_head_sha",
                        lambda _base, **_kw: "DIFFERENT_SHA")
    r = lg.check_merge_commit_sha(33, repo="dummy/repo")
    assert r.ok is False
    assert "mismatch" in r.reason.lower() or "불일치" in r.reason or "≠" in r.reason


# ── I. base branch ≠ main → 동적 검증 PASS ──
def test_I_nonmain_base_dynamic_pass(monkeypatch):
    monkeypatch.setattr(lg, "fetch_pr_merge_sha",
                        lambda _pr, _repo: {"merge_commit_sha": "abc123", "base_ref": "develop", "ok": True, "raw": {}})
    monkeypatch.setattr(lg, "fetch_origin_head_sha",
                        lambda base, **_kw: "abc123" if base == "develop" else "WRONG")
    r = lg.check_merge_commit_sha(50, repo="dummy/repo")
    assert r.ok is True, f"develop base에서도 동적으로 PASS: {r.as_dict()}"


# ── J. TASKCTL_BYPASS=1 + audit 없음 → fail ──
def test_J_bypass_without_audit_fails(tmp_path: Path):
    audit = tmp_path / "admin-override.jsonl"
    # 빈 audit
    audit.touch()
    r = lg.check_bypass_audit("task-test-J", env={"TASKCTL_BYPASS": "1"}, audit_path=audit)
    assert r.ok is False
    assert "audit" in r.reason.lower() or "bypass" in r.reason.lower()


# ── K. TASKCTL_PR_AUTHOR_OVERRIDE + audit 없음 → fail ──
def test_K_pr_author_override_without_audit_fails(tmp_path: Path):
    audit = tmp_path / "admin-override.jsonl"
    audit.touch()
    r = lg.check_bypass_audit("task-test-K",
                               env={"TASKCTL_PR_AUTHOR_OVERRIDE": "fake-bot"},
                               audit_path=audit)
    assert r.ok is False


# ── L. allowlist 외 author → merge 차단 ──
def test_L_unknown_author_blocks_merge(tmp_path: Path):
    cfg = tmp_path / "allowed_bot_accounts.json"
    cfg.write_text(json.dumps({
        "exact": ["jeon-jonghyuk-taskctl-bot[bot]"],
        "wildcard_suffix": ["[bot]"],
        "wildcard_prefix": ["app/"]
    }), encoding="utf-8")
    r = lg.check_bot_author_allowlist("malicious-user", allowlist_path=cfg)
    assert r.ok is False, f"unknown author blocked: {r.as_dict()}"


# ── M. fail → done transition 금지 ──
def test_M_fail_to_done_forbidden():
    r = lg.check_state_transition("FAILED", "DONE")
    assert r.ok is False, f"FAILED → DONE 금지: {r.as_dict()}"


# ── N. g3 PASS evidence 없음 → done 차단 ──
def test_N_no_g3_pass_evidence_blocks(tmp_evidence: Path):
    task_id = "task-test-N"
    # evidence 파일 미생성
    r = lg.check_done_g3_pass_evidence(task_id, pr_number=33, head_sha="abc",
                                        evidence_dir=tmp_evidence)
    assert r.ok is False
    assert "evidence" in r.reason.lower() or "g3" in r.reason.lower() or "missing" in r.reason.lower()
