"""tests/regression/test_post_merge_smoke_runner_2512.py — task-2512 회귀 12건.

회장 §1~12 매핑:
  1. PASS smoke
  2. FAIL smoke + Critical #7 packet
  3. TIMEOUT smoke + Critical #7 packet
  4. missing smoke + dry_run=True → SKIPPED
  5. missing smoke + dry_run=False → BLOCKED + escalation
  6. stdout head/tail capture
  7. stderr size cap
  8. JSON serialization round-trip
  9. Critical #7 enum 정확 매칭
 10. merge_commit propagation
 11. ★ replay fixtures (task-2506/2507/2509/2511)
 12. merge_queue continuation 신호 (allow_continuation)
"""
from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path
from typing import Optional

import pytest

WORKTREE = Path(__file__).resolve().parents[2]
if str(WORKTREE) in sys.path:
    sys.path.remove(str(WORKTREE))
sys.path.insert(0, str(WORKTREE))

from utils.automation_contracts import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    CriticalEscalationType,
    EscalationPacket,
    SmokeResult,
)
from utils.post_merge_smoke_runner import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    DEFAULT_OUTPUT_CAP_BYTES,
    REPLAY_FIXTURES,
    SMOKE_COMMAND_REGISTRY,
    SmokeStatus,
    build_smoke_failed_packet,
    run_post_merge_smoke,
)


# ---------------------------------------------------------------------------
# Fake runner factory — subprocess inject
# ---------------------------------------------------------------------------

class _FakeCompleted:
    def __init__(self, returncode: int, stdout: str = "", stderr: str = ""):
        self.returncode = returncode
        self.stdout = stdout
        self.stderr = stderr


def make_fake_runner(*, returncode: int = 0, stdout: str = "", stderr: str = "",
                    raise_timeout: bool = False, fetch_head: Optional[str] = None,
                    fetch_returncode: int = 0):
    """Args 첫 토큰 기준으로 git fetch / git rev-parse / smoke 분기 처리."""
    def _run(args, cwd=None, timeout=None):
        del cwd  # not used in tests
        if not args:
            return _FakeCompleted(0)
        if args[:2] == ["git", "fetch"]:
            return _FakeCompleted(fetch_returncode, stdout="", stderr="")
        if args[:2] == ["git", "rev-parse"]:
            return _FakeCompleted(fetch_returncode, stdout=(fetch_head or "0" * 40), stderr="")
        # smoke command
        if raise_timeout:
            raise subprocess.TimeoutExpired(cmd=" ".join(args), timeout=timeout or 0,
                                             output=stdout, stderr=stderr)
        return _FakeCompleted(returncode, stdout=stdout, stderr=stderr)
    return _run


# ---------------------------------------------------------------------------
# Fixture builders
# ---------------------------------------------------------------------------

def _write_task_md(tmp_path: Path, task_id: str, smoke_command: Optional[str] = None) -> Path:
    """task md 파일 생성. smoke_command 주어지면 yaml block에 list로 직렬화."""
    yaml_smoke = ""
    if smoke_command is not None:
        items = "\n".join(f'  - "{tok}"' for tok in smoke_command.split())
        yaml_smoke = f"\nsmoke_command:\n{items}\n"
    md = f"""# {task_id} — fixture for tests

```yaml
expected_files:
  - "utils/post_merge_smoke_runner.py"

risk_area: "post_merge_smoke / fixture"
parallel_policy: "limited_parallel"
merge_queue_position: 9
stale_recheck_required: true
cherry_pick_allowed: false{yaml_smoke}
```
"""
    p = tmp_path / f"{task_id}.md"
    p.write_text(md, encoding="utf-8")
    return p


# ===========================================================================
# 회장 §1 — PASS smoke
# ===========================================================================

def test_01_pass_smoke(tmp_path):
    task_md = _write_task_md(tmp_path, "task-9001", smoke_command="pytest -q")
    runner = make_fake_runner(returncode=0, stdout="ok\n", stderr="",
                              fetch_head="abc12345" * 5)
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit="abc12345" * 5, dry_run=False,
        runner=runner, pr_number=42, skip_stale_check=True,
    )
    assert run.status == SmokeStatus.PASS
    assert run.smoke_result.passed is True
    assert run.smoke_result.exit_code == 0
    assert run.smoke_result.failure_reason is None
    assert run.allow_continuation is True
    assert run.escalation is None
    assert run.smoke_command == ["pytest", "-q"]


# ===========================================================================
# 회장 §2 — FAIL smoke + Critical #7 packet
# ===========================================================================

def test_02_fail_smoke_creates_critical_7_packet(tmp_path):
    task_md = _write_task_md(tmp_path, "task-9002", smoke_command="pytest -q")
    runner = make_fake_runner(returncode=1, stdout="failed test\n", stderr="error log\n")
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit="bad" + "0" * 37, dry_run=False,
        runner=runner, pr_number=99, skip_stale_check=True,
    )
    assert run.status == SmokeStatus.FAIL
    assert run.smoke_result.passed is False
    assert run.smoke_result.exit_code == 1
    assert run.smoke_result.failure_reason == "EXIT_1"
    assert run.allow_continuation is False
    assert run.escalation is not None
    assert run.escalation.escalation_type == CriticalEscalationType.POST_MERGE_SMOKE_FAILED


# ===========================================================================
# 회장 §3 — TIMEOUT smoke + Critical #7 packet
# ===========================================================================

def test_03_timeout_smoke_creates_critical_7_packet(tmp_path):
    task_md = _write_task_md(tmp_path, "task-9003", smoke_command="pytest -q")
    runner = make_fake_runner(raise_timeout=True, stdout="partial\n", stderr="")
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit="t1" + "0" * 38, dry_run=False,
        runner=runner, pr_number=100, skip_stale_check=True,
    )
    assert run.status == SmokeStatus.TIMEOUT
    assert run.smoke_result.failure_reason == "TIMEOUT"
    assert run.escalation is not None
    assert run.escalation.escalation_type == CriticalEscalationType.POST_MERGE_SMOKE_FAILED
    assert run.allow_continuation is False


# ===========================================================================
# 회장 §4 — missing smoke + dry_run=True → SKIPPED
# ===========================================================================

def test_04_missing_smoke_dry_run_true_skipped(tmp_path):
    task_md = _write_task_md(tmp_path, "task-9004", smoke_command=None)
    runner = make_fake_runner(returncode=0)
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit="dry" + "0" * 37, dry_run=True,
        runner=runner, skip_stale_check=True,
    )
    assert run.status == SmokeStatus.SKIPPED
    assert run.allow_continuation is True
    assert run.escalation is None
    assert run.smoke_command is None


# ===========================================================================
# 회장 §5 — missing smoke + dry_run=False → BLOCKED + escalation
# ===========================================================================

def test_05_missing_smoke_dry_run_false_blocked(tmp_path):
    task_md = _write_task_md(tmp_path, "task-9005", smoke_command=None)
    runner = make_fake_runner(returncode=0)
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit="blk" + "0" * 37, dry_run=False,
        runner=runner, skip_stale_check=True,
    )
    assert run.status == SmokeStatus.BLOCKED
    assert run.allow_continuation is False
    assert run.escalation is not None
    assert run.escalation.escalation_type == CriticalEscalationType.POST_MERGE_SMOKE_FAILED
    assert run.smoke_command is None


# ===========================================================================
# 회장 §6 — stdout head/tail capture
# ===========================================================================

def test_06_stdout_head_tail_capture(tmp_path):
    task_md = _write_task_md(tmp_path, "task-9006", smoke_command="pytest -q")
    big = ("HEAD_LINE\n" * 5000) + ("TAIL_LINE\n" * 5000)  # ~100KB > 64KB cap
    runner = make_fake_runner(returncode=0, stdout=big, stderr="")
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit="cap" + "0" * 37, dry_run=False,
        runner=runner, skip_stale_check=True,
    )
    assert run.status == SmokeStatus.PASS
    out = run.smoke_result.stdout_tail
    assert "HEAD_LINE" in out  # 처음 보존
    assert "TAIL_LINE" in out  # 끝 보존
    assert "TRUNCATED" in out  # marker 존재
    assert len(out.encode("utf-8")) <= DEFAULT_OUTPUT_CAP_BYTES + 200  # marker 여분 허용


# ===========================================================================
# 회장 §7 — stderr size cap
# ===========================================================================

def test_07_stderr_size_cap(tmp_path):
    task_md = _write_task_md(tmp_path, "task-9007", smoke_command="pytest -q")
    huge = "ERROR_LINE\n" * 10000  # ~110KB
    runner = make_fake_runner(returncode=1, stdout="", stderr=huge)
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit="err" + "0" * 37, dry_run=False,
        runner=runner, skip_stale_check=True,
    )
    err = run.smoke_result.stderr_tail
    assert "ERROR_LINE" in err
    assert "TRUNCATED" in err
    assert len(err.encode("utf-8")) <= DEFAULT_OUTPUT_CAP_BYTES + 200


# ===========================================================================
# 회장 §8 — JSON serialization round-trip
# ===========================================================================

def test_08_json_serialization_round_trip(tmp_path):
    task_md = _write_task_md(tmp_path, "task-9008", smoke_command="pytest -q")
    runner = make_fake_runner(returncode=0, stdout="ok\n")
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit="js" + "0" * 38, dry_run=False,
        runner=runner, pr_number=11, skip_stale_check=True,
    )
    js = run.to_json()
    parsed = json.loads(js)
    for key in ("merge_commit", "task_id", "status", "smoke_result",
                "duration_ms", "smoke_command", "allow_continuation",
                "escalation", "stale", "dry_run"):
        assert key in parsed, f"missing key: {key}"
    assert parsed["status"] == "PASS"
    assert parsed["smoke_result"]["passed"] is True
    assert parsed["smoke_result"]["command"] == "pytest -q"


# ===========================================================================
# 회장 §9 — Critical #7 enum 정확 매칭
# ===========================================================================

def test_09_critical_7_enum_exact_match():
    sr = SmokeResult(
        command="pytest -q", passed=False, exit_code=1,
        stdout_tail="x", stderr_tail="y",
        failure_reason="EXIT_1",
    )
    packet = build_smoke_failed_packet(
        task_id="task-9009", pr_number=1,
        merge_commit="ee" + "0" * 38, smoke_result=sr,
        status=SmokeStatus.FAIL, smoke_command=["pytest", "-q"],
        duration_ms=100,
    )
    assert isinstance(packet, EscalationPacket)
    assert packet.escalation_type == CriticalEscalationType.POST_MERGE_SMOKE_FAILED
    assert packet.escalation_type.value == "POST_MERGE_SMOKE_FAILED"
    assert "POST_MERGE_SMOKE" in packet.escalation_type.value
    # safe_options + recommended_option 채워졌는지
    assert packet.safe_options
    assert packet.recommended_option


# ===========================================================================
# 회장 §10 — merge_commit propagation
# ===========================================================================

def test_10_merge_commit_propagation(tmp_path):
    sha = "deadbeef" * 5  # 40-char SHA
    task_md = _write_task_md(tmp_path, "task-9010", smoke_command="pytest -q")
    runner = make_fake_runner(returncode=2, stdout="", stderr="boom")
    run = run_post_merge_smoke(
        task_file=task_md, merge_commit=sha, dry_run=False,
        runner=runner, pr_number=70, skip_stale_check=True,
    )
    assert run.merge_commit == sha
    assert run.escalation is not None
    assert run.escalation.evidence["merge_commit"] == sha
    assert run.escalation.evidence["task_id"] == "task-9010"


# ===========================================================================
# 회장 §11 — ★ replay smoke fixtures (task-2506/2507/2509/2511)
# ===========================================================================

import os

_REAL_FIXTURE_DIR = Path(
    os.environ.get("WORKSPACE_ROOT", str(WORKTREE.parent.parent))
) / "memory" / "tasks"


@pytest.mark.parametrize("task_id", ["task-2506", "task-2507", "task-2509", "task-2511"])
def test_11_replay_fixtures_pass(tmp_path, task_id):
    """4 replay fixture 모두 동일 결과 재현 — PASS + FAIL 흐름.

    회장 §9 — task-2506 / task-2507 / task-2509 / task-2511 4개 fixture를 회귀 입력으로 사용.
    실제 task md 파일이 있으면 그것을 우선 사용 (강한 검증), 없으면 임시 fixture로 fallback.
    """
    # registry에 등록된 task_id가 smoke_command를 회수하는지 확인
    cmd = SMOKE_COMMAND_REGISTRY[task_id]
    assert cmd, f"SMOKE_COMMAND_REGISTRY missing {task_id}"
    fixture_meta = REPLAY_FIXTURES[task_id]
    assert fixture_meta, f"REPLAY_FIXTURES missing {task_id}"
    sha = fixture_meta["merge_commit_hint"] + "0" * (40 - len(fixture_meta["merge_commit_hint"]))

    # 실제 task md 파일을 우선 사용 (회장 §9 강한 검증 — 실제 입력)
    real_md = _REAL_FIXTURE_DIR / f"{task_id}.md"
    task_md = real_md if real_md.exists() else _write_task_md(tmp_path, task_id, smoke_command=None)

    # 동일 입력 → 동일 결과 (PASS) 재현
    pass_runner = make_fake_runner(returncode=0, stdout=f"{task_id} smoke ok\n")
    run_pass = run_post_merge_smoke(
        task_file=task_md, merge_commit=sha, dry_run=False,
        runner=pass_runner, skip_stale_check=True,
    )
    assert run_pass.status == SmokeStatus.PASS
    assert run_pass.smoke_command == cmd, f"registry fallback mismatch for {task_id}"
    assert run_pass.task_id == task_id
    assert run_pass.allow_continuation is True
    assert run_pass.merge_commit == sha
    assert run_pass.escalation is None

    # 동일 입력 → 동일 결과 (FAIL) 재현 — 결정성 검증 (같은 입력, 같은 출력)
    fail_runner = make_fake_runner(returncode=1, stderr="boom")
    run_fail = run_post_merge_smoke(
        task_file=task_md, merge_commit=sha, dry_run=False,
        runner=fail_runner, skip_stale_check=True,
    )
    assert run_fail.status == SmokeStatus.FAIL
    assert run_fail.task_id == task_id
    assert run_fail.escalation is not None
    assert run_fail.escalation.escalation_type == CriticalEscalationType.POST_MERGE_SMOKE_FAILED
    assert run_fail.escalation.evidence["task_id"] == task_id
    assert run_fail.escalation.evidence["merge_commit"] == sha


# ===========================================================================
# 회장 §12 — merge_queue continuation 신호 (5상태 모두)
# ===========================================================================

def test_12_continuation_signals_for_all_states(tmp_path):
    sha = "ccc" + "0" * 37
    # PASS
    task_md_pass = _write_task_md(tmp_path, "task-9012a", smoke_command="pytest -q")
    pass_run = run_post_merge_smoke(
        task_file=task_md_pass, merge_commit=sha, dry_run=False,
        runner=make_fake_runner(returncode=0), skip_stale_check=True,
    )
    assert pass_run.allow_continuation is True

    # FAIL
    fail_run = run_post_merge_smoke(
        task_file=task_md_pass, merge_commit=sha, dry_run=False,
        runner=make_fake_runner(returncode=1), skip_stale_check=True,
    )
    assert fail_run.allow_continuation is False

    # TIMEOUT
    to_run = run_post_merge_smoke(
        task_file=task_md_pass, merge_commit=sha, dry_run=False,
        runner=make_fake_runner(raise_timeout=True), skip_stale_check=True,
    )
    assert to_run.allow_continuation is False

    # SKIPPED (no command + dry_run)
    task_md_skip = _write_task_md(tmp_path, "task-9012b", smoke_command=None)
    skip_run = run_post_merge_smoke(
        task_file=task_md_skip, merge_commit=sha, dry_run=True,
        runner=make_fake_runner(returncode=0), skip_stale_check=True,
    )
    assert skip_run.allow_continuation is True

    # BLOCKED (no command + non-dry-run)
    blk_run = run_post_merge_smoke(
        task_file=task_md_skip, merge_commit=sha, dry_run=False,
        runner=make_fake_runner(returncode=0), skip_stale_check=True,
    )
    assert blk_run.allow_continuation is False

    # ★ stale 게이트: PASS여도 origin/main HEAD ≠ merge_commit이면 큐 진행 차단.
    # fetch_head를 다른 SHA로 주입해 stale=True 시나리오 재현.
    stale_runner = make_fake_runner(returncode=0, fetch_head="0" * 40)
    stale_run = run_post_merge_smoke(
        task_file=task_md_pass, merge_commit=sha, dry_run=False,
        runner=stale_runner, skip_stale_check=False,  # ★ stale check 활성화
    )
    assert stale_run.status == SmokeStatus.PASS
    assert stale_run.stale is True
    assert stale_run.allow_continuation is False  # PASS여도 stale이면 차단
