"""선언 좌표(시대·지역)에 **production 이 쓰는 입구**가 있다 (Codex BLOCK 3, 2026-09-02).

앞 판은 읽는 쪽(`grounding_coordinates`)과 canary 주입만 있었다 — 일반 사용자는 여전히
모델 추론값으로 떨어져 「1960년대 → 일제강점기」변질이 되살아난다. 여기서는
  ① 순수 helper 둘(적기/지우기 · 스텝 설정 저장이 스칼라 키를 남김)
  ② 라우트가 실제로 등록돼 있다(PUT·GET)
  ③ 핸들러가 같은 저장 helper 를 지나 `llm_config_json` 에 남긴다(가짜 세션으로 끝점)
  ④ `visual_world_rules` 지문이 좌표 계약 판을 접는다 — 계약 전 CP 가 재사용되지 않는다
"""
from __future__ import annotations

import json

import pytest


class TestHelpers:
    def test_declaring_and_clearing(self):
        from app.modules.pipeline.grounding_coordinates import with_declared_coordinates as w

        got = w({"gemini": {"provider": "x"}}, era="1960년대", region="대한민국")
        assert got["grounding_era"] == "1960년대" and got["grounding_region"] == "대한민국"
        assert got["gemini"] == {"provider": "x"}
        cleared = w(got, era="  ", region=None)
        assert "grounding_era" not in cleared and "grounding_region" not in cleared

    def test_step_config_save_keeps_scalar_keys(self):
        from app.modules.pipeline.grounding_coordinates import merge_step_config

        existing = {"grounding_era": "1960년대", "grounding_mode": "v2_chunk",
                    "beat_extract": {"provider": "old", "model": "o"}}
        got = merge_step_config(existing, {"beat_extract": {"provider": "new", "model": "n"}})
        assert got["beat_extract"]["provider"] == "new"
        assert got["grounding_era"] == "1960년대" and got["grounding_mode"] == "v2_chunk"
        # 스텝 키는 통째로 바뀐다 — 옛 스텝 키가 남지 않는다
        assert "old" not in json.dumps(got)


class _FakeQuery:
    def __init__(self, row):
        self._row = row

    def filter(self, *a, **k):
        return self

    def first(self):
        return self._row


class _FakeSession:
    def __init__(self, row=None, by_model=None):
        self.row = row
        self.by_model = dict(by_model or {})
        self.added = []
        self.commits = 0

    def query(self, model):
        name = getattr(model, "__name__", str(model))
        if name in self.by_model:
            return _FakeQuery(self.by_model[name])
        return _FakeQuery(self.row)

    def add(self, obj):
        self.added.append(obj)
        self.row = obj

    def commit(self):
        self.commits += 1


class _User:
    def __init__(self, role, uid="u1"):
        self.role = role
        self.id = uid


class _Member:
    def __init__(self, role):
        self.role = role


class TestTheWriteGate:
    """★Codex BLOCK 2 (2026-09-02 밤): 좌표 PUT 은 다음 유료 조사의 좌표와 config hash 를 바꾸는
    production 입력이다 — member(읽기)는 403 · DB 무변, owner/admin 만 저장."""

    def _db(self, member_role):
        return _FakeSession(row=None, by_model={"ProjectRegistry": object(),
                                                "ProjectMember": _Member(member_role) if member_role else None})

    def test_a_reader_member_is_refused_and_nothing_is_written(self):
        from app.api.v1 import projects as api
        from app.core.errors import AppError

        db = self._db("member")
        with pytest.raises(AppError) as exc:
            api.update_grounding_coordinates(api.GroundingCoordinatesBody(era="1960년대", region="대한민국"),
                                             project_id="p1", db=db, current_user=_User("user"))
        assert exc.value.status_code == 403 and db.commits == 0 and not db.added

    def test_an_owner_member_writes(self):
        from app.api.v1 import projects as api

        db = self._db("owner")
        got = api.update_grounding_coordinates(api.GroundingCoordinatesBody(era="1960년대", region=None),
                                               project_id="p1", db=db, current_user=_User("user"))
        assert got["era"] == "1960년대" and db.commits == 1

    def test_the_fidelity_post_and_the_put_share_one_gate(self):
        from app.api import deps
        from app.api.v1 import grounding_fidelity as gf

        assert gf.require_project_write is deps.require_project_write


class TestTheRouteAndHandler:
    def test_routes_are_registered(self):
        from app.main import app

        paths = {(r.path, m) for r in app.routes for m in (getattr(r, "methods", None) or ())}
        assert ("/api/v1/projects/{project_id}/grounding-coordinates", "PUT") in paths
        assert ("/api/v1/projects/{project_id}/grounding-coordinates", "GET") in paths

    def test_handler_writes_the_keys_the_pipeline_reads(self):
        from app.api.v1 import projects as api
        from app.modules.pipeline.grounding_coordinates import declared_coordinates

        db = _FakeSession(row=None, by_model={"ProjectRegistry": object()})
        got = api.update_grounding_coordinates(
            api.GroundingCoordinatesBody(era="1960년대", region="대한민국"),
            project_id="p1", db=db, current_user=_User("admin"))
        assert got == {"ok": True, "era": "1960년대", "region": "대한민국"}
        assert db.commits == 1 and db.added
        stored = json.loads(db.added[0].llm_config_json)
        # ★파이프라인이 읽는 그 키 — 읽는 쪽 함수로 다시 읽어 같은 값이 나온다
        assert declared_coordinates(stored) == {"era": "1960년대", "region": "대한민국"}

    def test_llm_config_save_no_longer_erases_the_declaration(self):
        from app.api.v1 import projects as api

        class _Row:
            llm_config_json = json.dumps({"grounding_era": "1960년대", "x": {"provider": "a", "model": "b"}})
            updated_at = ""

        db = _FakeSession(row=_Row())
        api.update_llm_config(api.LLMConfigUpdate(config={"x": {"provider": "c", "model": "d"}}),
                              project_id="p1", db=db, current_user=object())
        stored = json.loads(db.row.llm_config_json)
        assert stored["grounding_era"] == "1960년대" and stored["x"] == {"provider": "c", "model": "d"}
