"""시나리오 축이 설정 셋(배경 · still_recipe · outdoor)으로 넓어졌다 — 전부 **실제 설정**에서 읽고
CLI 선언은 대조만 한다.

★2단계(이미지) closure 에 outdoor 사슬과 still_recipe 사슬이 들어온다(전부 unverified 단위).
Codex ① (2026-09-02): 고증 canary 두 arm 의 공통 설정으로만 기록하고 production 기본값
변경으로 읽지 않는다. 배경 축과 같은 규칙: process 가 뜰 때 고정된 settings 를 술어로 읽고,
선언이 다르면 선다, 뒤집어 같은 run 을 이으려면 끝난 CP 중 그 설정을 접는 스텝이 없어야 한다.
"""
from __future__ import annotations

import pytest

from tools.grounding_audit import canary_run as cr


@pytest.fixture
def lanes(monkeypatch):
    from app.core.config import settings

    def _set(*, background="off", still_recipe="off", outdoor="off"):
        monkeypatch.setattr(settings, "background_mode", background)
        monkeypatch.setattr(settings, "still_recipe_mode", "v1" if still_recipe == "on" else "off")
        on = outdoor == "on"
        monkeypatch.setattr(settings, "outdoor_lane_plan_enabled", on)
        monkeypatch.setattr(settings, "outdoor_lane_pipe_enabled", on)
        monkeypatch.setattr(settings, "outdoor_direct_compose_enabled", False)
        monkeypatch.setattr(settings, "outdoor_map_conti_enabled", False)
    return _set


class TestTheAxesReadTheActualSettings:
    def test_all_three_axes_are_scenario_axes(self):
        for axis in ("background", "still_recipe", "outdoor"):
            assert axis in cr._SCENARIO_AXES
            assert axis in cr._SETTING_AXES

    def test_off_off_off(self, lanes):
        lanes()
        assert cr.axis_actual("background") == "off"
        assert cr.axis_actual("still_recipe") == "off"
        assert cr.axis_actual("outdoor") == "off"

    def test_still_recipe_v1_is_on(self, lanes):
        lanes(still_recipe="on")
        assert cr.axis_actual("still_recipe") == "on"

    def test_outdoor_any_lane_flag_is_on(self, lanes, monkeypatch):
        from app.core.config import settings

        lanes()
        monkeypatch.setattr(settings, "outdoor_lane_plan_enabled", True)   # plan 만 켜도 on
        assert cr.axis_actual("outdoor") == "on"
        lanes()
        monkeypatch.setattr(settings, "outdoor_map_conti_enabled", True)    # map 도 on
        assert cr.axis_actual("outdoor") == "on"

    def test_the_scenario_records_every_axis(self, lanes):
        lanes(still_recipe="on")
        sc = cr.scenario(mode="v2_chunk", fixture="period_episode", target="scene_image_pipeline")
        assert sc["background"] == "off" and sc["still_recipe"] == "on" and sc["outdoor"] == "off"

    def test_a_declaration_that_lies_is_refused(self, lanes):
        """★양성 대조 — `--still-recipe off` 라 해 놓고 process 가 v1 이면 선다."""
        lanes(still_recipe="on")
        with pytest.raises(cr.ScopeMismatch, match="still_recipe"):
            cr.scenario(mode="v2_chunk", fixture="period_episode",
                        target="scene_image_pipeline", still_recipe="off")
        with pytest.raises(cr.ScopeMismatch, match="outdoor"):
            lanes(outdoor="on")
            cr.scenario(mode="v2_chunk", fixture="period_episode",
                        target="scene_image_pipeline", outdoor="off")

    def test_a_declaration_that_matches_passes(self, lanes):
        lanes()
        sc = cr.scenario(mode="v2_chunk", fixture="period_episode", target="scene_image_pipeline",
                         background="off", still_recipe="off", outdoor="off")
        assert (sc["background"], sc["still_recipe"], sc["outdoor"]) == ("off", "off", "off")


class TestTheFixtureDeclaresWhatProductionCannotAnswer:
    def test_has_outlooks_comes_from_the_fixture(self, lanes):
        lanes()
        cfg = cr.fixture_config("v2_chunk", fixture="period_episode")
        assert cfg["has_outlooks"] is True
        assert cfg["outdoor_direct_or_map_or_lane"] is False
        assert cfg["background_share_plan"] is False          # still_recipe off → False

    def test_without_a_fixture_has_outlooks_is_not_declared(self, lanes):
        lanes()
        assert "has_outlooks" not in cr.fixture_config("v2_chunk")

    def test_the_plan_no_longer_has_unknown_steps_with_lanes_off(self, lanes):
        """★실측 2026-09-02: composite_image_gen · outdoor_place_spec · outdoor_place_canon ·
        background_share_plan 넷이 `unknown` 이라 live 가 섰다. 선언 뒤엔 0."""
        lanes()
        built = cr.build_plan(cr.scenario(mode="v2_chunk", fixture="period_episode",
                                          target="scene_image_pipeline"))
        plan = built["plan"]
        unknown = [x["step"] if isinstance(x, dict) else x for x in plan["unknown"]]
        assert unknown == [], unknown
        applied = [x["step"] if isinstance(x, dict) else x for x in plan["applied"]]
        assert "composite_image_gen" in applied
        for lane_step in ("outdoor_lane_plan", "outdoor_structure_seed", "shot_conti_light",
                          "shot_ref_classify", "shot_continuity", "background_share_plan",
                          "outdoor_place_spec", "outdoor_place_canon"):
            assert lane_step not in applied, lane_step


class TestFlippingALaneOnAResumedRun:
    def test_flip_is_safe_when_no_completed_step_folds_the_setting(self, lanes, monkeypatch):
        lanes()
        monkeypatch.setattr(cr, "previous_axis", lambda run_id, axis: "on")
        monkeypatch.setattr(cr, "completed_steps_of", lambda run_id, **k: ["scene_detail", "world_guide"])
        got = cr.assert_setting_flip_is_safe("r", {"background": "off", "still_recipe": "off", "outdoor": "off"})
        assert all(v["flipped"] for v in got["axes"].values())
        assert all(v["stale"] == [] for v in got["axes"].values())

    def test_flip_is_refused_when_a_completed_step_folds_it(self, lanes, monkeypatch):
        """★양성 대조 — still_recipe 를 접는 shot_conti_light 가 끝나 있으면 선다."""
        lanes()
        monkeypatch.setattr(cr, "previous_axis", lambda run_id, axis: "on")
        monkeypatch.setattr(cr, "completed_steps_of", lambda run_id, **k: ["shot_conti_light"])
        with pytest.raises(cr.ScopeMismatch, match="still_recipe"):
            cr.assert_setting_flip_is_safe("r", {"background": "off", "still_recipe": "off", "outdoor": "off"})


class TestTheRunIdentityIsNotRetyped:
    """★실측 2026-09-02: 전이를 적으며 `--fixture` 를 빠뜨려 CLI 기본값이 period_episode
    run 의 장부에 적혔다. run 이 아는 신원(mode·fixture)과 다르면 선다."""

    def _root(self, tmp_path, monkeypatch, scenario):
        import json

        monkeypatch.setenv("THEROAD_CANARY_ROOT", str(tmp_path))
        d = tmp_path / "canary_1dabc123abc1"
        d.mkdir(parents=True, exist_ok=True)
        rec = {"code": {"tip": "a" * 40, "clean": True}}
        if scenario is not None:
            rec["scenario"] = scenario
        (d / "canary_run.json").write_text(json.dumps(rec), encoding="utf-8")
        return "1dabc123abc1"

    def test_a_different_fixture_is_refused(self, tmp_path, monkeypatch):
        rid = self._root(tmp_path, monkeypatch, {"mode": "v2_chunk", "fixture": "period_episode"})
        with pytest.raises(cr.ScopeMismatch, match="신원"):
            cr.assert_scenario_matches_run(rid, {"mode": "v2_chunk", "fixture": "canary_one_scene"})
        with pytest.raises(cr.ScopeMismatch, match="신원"):
            cr.assert_scenario_matches_run(rid, {"mode": "legacy", "fixture": "period_episode"})

    def test_the_same_identity_passes_even_if_the_target_changes(self, tmp_path, monkeypatch):
        rid = self._root(tmp_path, monkeypatch,
                         {"mode": "v2_chunk", "fixture": "period_episode", "target": "world_guide"})
        got = cr.assert_scenario_matches_run(
            rid, {"mode": "v2_chunk", "fixture": "period_episode", "target": "scene_image_pipeline"})
        assert got == {"mode": "v2_chunk", "fixture": "period_episode"}

    def test_an_old_record_without_a_scenario_cannot_be_checked(self, tmp_path, monkeypatch):
        rid = self._root(tmp_path, monkeypatch, None)
        assert cr.assert_scenario_matches_run(rid, {"mode": "legacy", "fixture": "x"}) is None

    def test_the_resume_gate_checks_it_before_anything_else(self, tmp_path, monkeypatch):
        rid = self._root(tmp_path, monkeypatch, {"mode": "v2_chunk", "fixture": "period_episode"})
        with pytest.raises(cr.ScopeMismatch, match="신원"):
            cr.assert_resume_transition(rid, {"mode": "v2_chunk", "fixture": "canary_one_scene",
                                              "target": "scene_image_pipeline"})
