"""끝난 스텝을 다시 들어갈 때 — runner 의 지문 어긋남을 **먼저 묻고**, 승인된 전이가 있으면 force.

★실측 2026-09-02 밤: runner 가 `grounding_chunk contract drift detected … force 재실행 필요`
(409) 를 올렸는데 `run_steps_batch` 가 삼켰고, canary 는 옛 CP 를 completed 로 되읽어
지나갔다 — 새 처리 계약이 하류에 하나도 안 닿았다.
"""
from __future__ import annotations

import ast
import json
from pathlib import Path

import pytest

from tools.grounding_audit import canary_pipeline as cp


class TestPlanReentryTable:
    def test_clean_completed_step_is_reused_with_cap_zero(self):
        assert cp.plan_reentry("completed", None, False, False) == {"mode": "resume", "cap_zero": True, "stop_why": None}

    def test_drift_with_an_approved_transition_forces(self):
        got = cp.plan_reentry("completed", "config_hash mismatch", True, False)
        assert got["mode"] == "force" and got["cap_zero"] is False

    def test_drift_without_a_transition_stops(self):
        got = cp.plan_reentry("completed", "config_hash mismatch", False, False)
        assert got["mode"] is None and "전이" in got["stop_why"]

    def test_unfinished_runs_normally_and_a_reopened_completed_step_is_forced(self):
        assert cp.plan_reentry(None, None, True, False)["mode"] == "resume"
        got = cp.plan_reentry("completed", "x", True, True)
        assert got["mode"] == "force" and got["cap_zero"] is False, "★지목한 끝난 스텝은 다시 산다"
        assert cp.plan_reentry("partial", None, True, True)["mode"] == "resume"


class TestTheHelpersAskTheRealThings:
    def test_drift_comes_from_the_runners_own_check(self, monkeypatch):
        from app.services import analysis_dispatch_service as ads

        class _R:
            def load_checkpoint(self): return {"config_hash": "old"}
            def _check_cp_mismatch(self, cp): return "config_hash mismatch (step-local): cp=old, current=new"

        monkeypatch.setattr(ads, "get_step_runner", lambda *a, **k: _R())
        monkeypatch.setattr(cp, "SessionLocal", lambda: type("S", (), {"close": lambda self: None})(), raising=False)
        from app.core import database as dbm
        monkeypatch.setattr(dbm, "SessionLocal", lambda: type("S", (), {"close": lambda self: None})())
        assert "mismatch" in cp.contract_drift_of("grounding_chunk", "p", "e", {})

        class _Clean(_R):
            def _check_cp_mismatch(self, cp): return None
        monkeypatch.setattr(ads, "get_step_runner", lambda *a, **k: _Clean())
        assert cp.contract_drift_of("grounding_chunk", "p", "e", {}) is None

    def test_transition_must_point_at_head(self, tmp_path):
        """★`git_tip()` 을 **진짜로** 부른다 — 대역을 str 로 만들었다가 실제 dict 를 못 읽어
        전이가 있는데도 「없다」로 섰다 (실측 2026-09-02 밤, attempt 8af63115)."""
        from tools.grounding_audit import canary_run as cr

        head = cr.git_tip()["tip"]
        assert cp.code_transition_covers_head(tmp_path) is False
        (tmp_path / "pipeline_attempts.json").write_text(json.dumps([
            {"attempt_id": "x", "status": "stopped"},
            {"kind": "code_transition", "from_tip": "000", "to_tip": head}]), encoding="utf-8")
        assert cp.code_transition_covers_head(tmp_path) is True
        (tmp_path / "pipeline_attempts.json").write_text(json.dumps([
            {"kind": "code_transition", "from_tip": "000", "to_tip": head},
            {"kind": "code_transition", "from_tip": head, "to_tip": "fff"}]), encoding="utf-8")
        assert cp.code_transition_covers_head(tmp_path) is False, "★마지막 전이가 다른 코드를 가리킨다"

    def test_not_applicable_steps_are_not_asked_for_drift(self):
        """돌지 않는 스텝의 어긋남은 뜻이 없다 — 표는 cap 0 이다."""
        assert cp.plan_reentry("not_applicable", None, False, False)["cap_zero"] is True
        src = __import__("inspect").getsource(cp.run_pipeline)
        assert 'if durable == "completed" else None' in src


class TestTheChunkStepPersistsItsOwnFingerprint:
    def test_execute_returns_config_hash_next_to_the_quarantine_ledger(self):
        """★안 적으면 base 해시가 저장되고 매 재개가 drift→BLOCK 이다 — AST 로 본다."""
        src = (Path(__file__).resolve().parents[2] / "app" / "core" / "steps" / "grounding_chunk_step.py").read_text(encoding="utf-8")
        tree = ast.parse(src)
        hits = []
        for node in ast.walk(tree):     # ★반환은 `_execute` 안의 `_wrap` 에 있다 — 모듈 전체를 본다
            if isinstance(node, ast.Return) and isinstance(node.value, ast.Dict):
                keys = {k.value for k in node.value.keys if isinstance(k, ast.Constant)}
                inner = json.dumps([ast.dump(v) for v in node.value.values])
                if "grounding_quarantined" in inner:
                    hits.append(keys)
        assert hits and all("config_hash" in k for k in hits), hits


class TestThereIsNoSelfOnlyRerunLane:
    """★Codex BLOCK 2026-09-03: 「자기만 다시 돌고 하류 보존」갈래는 없다 — 다시 도는 스텝은 LLM 을 부르므로 계보가 끊긴다.
    hash 조리법만 바뀐 경우는 `canary_hash_adoption.adopt` 로 어긋남을 없애 첫 줄(cap 0)로 들어온다."""

    def test_plan_reentry_has_no_acked_axis(self):
        import inspect
        from tools.grounding_audit import canary_pipeline as cp
        assert "acked" not in inspect.signature(cp.plan_reentry).parameters
        got = cp.plan_reentry("completed", "config_hash mismatch", True, False)
        assert got["mode"] == "force"

    def test_an_adopted_step_has_no_drift_and_is_reused_at_cap_zero(self):
        from tools.grounding_audit import canary_pipeline as cp
        assert cp.plan_reentry("completed", None, True, False)["cap_zero"] is True
