"""W21B-w5 STEP5-B commit④: background_render light-FP sidecar consumer.

Unit tests for ``_apply_light_fp_sidecar`` — the single injection point that
swaps the detailed FP anchor PNG for the simplified light sidecar (feeding BOTH
the legacy ``_process`` and the shot_aware substrate paths via ``fp_paths_str``).
Default (flag OFF) is byte-identical: the helper returns {} and does not touch
``fp_paths_str``. A missing/invalid sidecar falls back to the detailed FP per fp.
"""
from __future__ import annotations

from unittest.mock import MagicMock, patch

from app.core.steps.background_render_step import BackgroundRenderStep


def _new_step(*, cp_map: dict) -> BackgroundRenderStep:
    step = BackgroundRenderStep.__new__(BackgroundRenderStep)
    step.project_id = "p"
    step.episode_id = "e"
    step._load_prev_checkpoint = MagicMock(side_effect=lambda sid: cp_map.get(sid))
    return step


def _sidecar_cp(per_fp: dict) -> dict:
    return {"floor_plan_light_sidecar": {"data": {"per_fp": per_fp}}}


def _run_apply(step, fp_paths_str, *, enabled, tmp_path, rel_to_abs=None):
    patches = [
        patch("app.core.config.settings.floor_plan_light_sidecar_enabled", enabled),
        patch("app.core.config.settings.projects_dir", str(tmp_path / "projects")),
    ]
    for p in patches:
        p.start()
    try:
        if rel_to_abs is not None:
            with patch(
                "app.core.file_paths.resolve_image_path",
                side_effect=rel_to_abs,
            ):
                return step._apply_light_fp_sidecar(fp_paths_str)
        return step._apply_light_fp_sidecar(fp_paths_str)
    finally:
        for p in reversed(patches):
            p.stop()


# ──────────────────────────── flag OFF (byte-identical) ────────────────────────────
def test_flag_off_no_op(tmp_path):
    step = _new_step(cp_map=_sidecar_cp({"fp_a": {"status": "rendered",
                                                  "light_png_relative_path": "x.png"}}))
    fp_paths = {"fp_a": "/abs/detailed/fp_a.png"}
    prov = _run_apply(step, fp_paths, enabled=False, tmp_path=tmp_path)
    assert prov == {}
    assert fp_paths == {"fp_a": "/abs/detailed/fp_a.png"}  # untouched


def test_flag_on_no_cp(tmp_path):
    step = _new_step(cp_map={})
    fp_paths = {"fp_a": "/abs/detailed/fp_a.png"}
    prov = _run_apply(step, fp_paths, enabled=True, tmp_path=tmp_path)
    assert prov == {}
    assert fp_paths == {"fp_a": "/abs/detailed/fp_a.png"}


# ──────────────────────────── swap path ────────────────────────────
def test_rendered_swaps_to_light(tmp_path):
    light = tmp_path / "light" / "fp_a.png"
    light.parent.mkdir(parents=True)
    light.write_bytes(b"png")
    step = _new_step(cp_map=_sidecar_cp({
        "fp_a": {"status": "rendered", "light_png_relative_path": "rel/fp_a.png"},
    }))
    fp_paths = {"fp_a": "/abs/detailed/fp_a.png", "fp_b": "/abs/detailed/fp_b.png"}
    prov = _run_apply(
        step, fp_paths, enabled=True, tmp_path=tmp_path,
        rel_to_abs=lambda rel: light if rel == "rel/fp_a.png" else None,
    )
    # fp_a swapped to the light PNG; fp_b (no sidecar entry) keeps detailed.
    assert fp_paths["fp_a"] == str(light)
    assert fp_paths["fp_b"] == "/abs/detailed/fp_b.png"
    assert prov["fp_a"] == {"used": "light", "light_png": "rel/fp_a.png"}
    assert prov["fp_b"] == {"used": "detailed", "reason": "absent"}


def test_rendered_but_png_missing_falls_back(tmp_path):
    step = _new_step(cp_map=_sidecar_cp({
        "fp_a": {"status": "rendered", "light_png_relative_path": "rel/fp_a.png"},
    }))
    fp_paths = {"fp_a": "/abs/detailed/fp_a.png"}
    prov = _run_apply(
        step, fp_paths, enabled=True, tmp_path=tmp_path,
        rel_to_abs=lambda rel: None,  # resolves to nothing on disk
    )
    assert fp_paths["fp_a"] == "/abs/detailed/fp_a.png"  # detailed kept
    assert prov["fp_a"] == {"used": "detailed", "reason": "light_png_missing"}


def test_fallback_status_keeps_detailed(tmp_path):
    step = _new_step(cp_map=_sidecar_cp({
        "fp_a": {"status": "fallback", "fallback_reason": "validator_failed",
                 "light_png_relative_path": None},
    }))
    fp_paths = {"fp_a": "/abs/detailed/fp_a.png"}
    prov = _run_apply(step, fp_paths, enabled=True, tmp_path=tmp_path)
    assert fp_paths["fp_a"] == "/abs/detailed/fp_a.png"
    assert prov["fp_a"] == {"used": "detailed", "reason": "fallback"}


# ──────────────────────────── Codex R1: config_hash invalidation ────────────────────────────
def _config_hash_with_flag(step, *, flag):
    patches = [
        patch("app.core.config.settings.floor_plan_light_sidecar_enabled", flag),
    ]
    for p in patches:
        p.start()
    try:
        return step._config_hash()
    finally:
        for p in reversed(patches):
            p.stop()


def test_config_hash_stamps_sidecar_flag():
    # flag OFF → byte-identical to the pre-STEP5-B hash (key absent); flipping
    # the flag ON must change the hash so an existing render checkpoint is
    # invalidated (the FP anchor PNG fed to render changed).
    step = BackgroundRenderStep.__new__(BackgroundRenderStep)
    h_off = _config_hash_with_flag(step, flag=False)
    h_on = _config_hash_with_flag(step, flag=True)
    assert h_off != h_on


def test_config_hash_off_is_stable():
    step = BackgroundRenderStep.__new__(BackgroundRenderStep)
    assert _config_hash_with_flag(step, flag=False) == _config_hash_with_flag(
        step, flag=False
    )


def test_config_hash_folds_consumed_sidecar_config_hash():
    # Codex W21B-w5: with the flag ON, REGENERATING the sidecar (a new NB2 model /
    # selection prompt / best_of_n changes the sidecar's own config_hash) must
    # invalidate background_render too — not just flipping the flag. The consumed
    # sidecar config_hash is folded in, so two different sidecar hashes yield two
    # different background_render hashes.
    step = BackgroundRenderStep.__new__(BackgroundRenderStep)
    step.project_id = "p"
    step.episode_id = "e"

    def _hash_with_sidecar(sidecar_hash):
        step._load_prev_checkpoint = MagicMock(
            return_value={"config_hash": sidecar_hash})
        return _config_hash_with_flag(step, flag=True)

    h_a = _hash_with_sidecar("aaaa1111")
    h_b = _hash_with_sidecar("bbbb2222")
    assert h_a != h_b  # a sidecar regen propagates into background_render
