"""★소유권 전환(legacy 구매 → 중앙 보충)이 야외 스텝의 config hash 에 실린다 (Codex BLOCK 2026-09-03).

918e1fbb 는 manifest 의존과 runtime 소유자만 바꿨고 hash 는 안 움직였다 — 옛 legacy 구매형 completed CP 를 둔
v2/v2_chunk 재개가 새 중앙 보충 코드로 다시 들어가지 않고 그대로 되쓰일 자리였다. fresh run 만 돌리면 이 회귀가 숨는다."""
from __future__ import annotations

import hashlib
import json

import pytest

import app.core.steps.outdoor_structure_form_reference_step as m
from app.modules.pipeline import grounding_outdoor_supplement as gos
from app.modules.pipeline import reference_acquisition as ra


def _step(mode):
    st = m.OutdoorStructureFormReferenceStep.__new__(m.OutdoorStructureFormReferenceStep)
    st.project_id, st.episode_id = "p", "e"
    st.project_config = {"grounding_mode": mode}
    from app.core.step_manifest import STEP_MANIFEST
    st.manifest = STEP_MANIFEST["outdoor_structure_form_reference"]       # runner 의 schema 대조가 읽는다
    return st


def _legacy_hash_by_old_formula(st):
    """옛 코드의 공식 그대로 — payload 를 옛 자리에서 다시 만들어 sha256."""
    from app.core.config import settings
    from app.modules.pipeline.search_grounded_ref import (
        MAX_PICK_CANDIDATES, PICK_HEAD_CONTRACT_VERSION, PICK_JUDGES, SAFE_DOWNLOAD_POLICY_VERSION,
        SEARCH_IMAGE_RESULTS, SEARCH_ORCHESTRATOR, TARGET_POLICY_VERSION, build_web_search_tool,
        resolve_ref_pack_version)
    payload = st._config_payload_legacy(settings, resolve_ref_pack_version, PICK_HEAD_CONTRACT_VERSION,
                                        build_web_search_tool, SEARCH_IMAGE_RESULTS, MAX_PICK_CANDIDATES,
                                        SEARCH_ORCHESTRATOR, SAFE_DOWNLOAD_POLICY_VERSION, PICK_JUDGES,
                                        TARGET_POLICY_VERSION)
    assert "central_ownership" not in payload
    return hashlib.sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest()


class TestTheLegacyHashDoesNotMove:
    def test_legacy_mode_hash_is_byte_identical_to_the_old_payload(self, monkeypatch):
        st = _step("legacy")
        assert st._config_hash() == _legacy_hash_by_old_formula(st)
        # ★사지 않는 모드는 중앙 계약이 바뀌어도 hash 가 안 움직인다
        before = st._config_hash()
        monkeypatch.setattr(gos, "SUPPLEMENT_CONTRACT_VERSION", "9.999")
        assert st._config_hash() == before


class TestABuyingModeBindsTheCutover:
    @pytest.mark.parametrize("mode", ["v2", "v2_chunk"])
    def test_the_hash_differs_from_legacy_and_names_the_owner(self, mode):
        st = _step(mode)
        assert st._config_hash() != _step("legacy")._config_hash()
        b = st._central_ownership_binding()["central_ownership"]
        assert b["owner"] == ra.OWNER_FRONT and b["grounding_mode"] == mode
        assert b["supplement_contract"] == gos.SUPPLEMENT_CONTRACT_VERSION
        assert b["front_projection_contract"] == m.FRONT_PROJECTION_CONTRACT_VERSION

    @pytest.mark.parametrize("where, name", [
        (gos, "SUPPLEMENT_CONTRACT_VERSION"), (m, "FRONT_PROJECTION_CONTRACT_VERSION"), (ra, "ACQUISITION_CONTRACT_VERSION")])
    def test_changing_any_one_contract_moves_the_hash(self, monkeypatch, where, name):
        st = _step("v2_chunk")
        before = st._config_hash()
        cur = getattr(where, name)
        monkeypatch.setattr(where, name, (cur + 1) if isinstance(cur, int) else str(cur) + ".x")
        assert st._config_hash() != before


class TestAnOldLegacyCheckpointDriftsUnderTheNewCode:
    def test_the_runner_reports_the_mismatch_so_the_canary_forces_the_step(self, monkeypatch):
        """★끝점: 옛 판(legacy 구매형)의 CP 는 legacy hash 를 들고 있다. 사는 모드의 runner 가 그 CP 를
        읽으면 어긋남을 말해야 재개가 force 로 들어간다(`contract_drift_of` 가 부르는 `_check_cp_mismatch`)."""
        from app.core.step_runner import StepRunner
        st = _step("v2_chunk")
        old_cp = {"config_hash": _step("legacy")._config_hash(), "schema_version": m.SCHEMA_VERSION,
                  "data": {"groups": {"g1": {"status": "ok", "round_id": "r001"}}}}
        why = StepRunner._check_cp_mismatch(st, old_cp)
        assert why, "옛 legacy CP 가 사는 모드에서 어긋남 없이 지나간다"
