"""REC-247 — a PERMANENT-attachment worn prop (e.g. the debt counter) with a hero
ref gets the ref fed to the board gen, LABELED worn, so the model matches its exact
appearance instead of inventing a garbled one each panel. The worn prose still emits
and the "never free-floating" label preserves the REC-219 protection. Honors the
auto-inject toggle and an explicit suppress_worn override (no ref in either case).
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

from PIL import Image

_REPO_ROOT = Path(__file__).resolve().parents[4]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from recoil.core.paths import ProjectPaths  # noqa: E402
from recoil.pipeline._lib.board_builder import (  # noqa: E402
    _append_prop_refs,
    _character_descriptions,
)
from recoil.pipeline._lib.prompt_engine import build_storyboard_strip_prompt  # noqa: E402

_PROP = "debt_counter"
_DESC = "Amber-glowing wrist device strapped to the left wrist, LCD counter display."


def _proj(tmp_path) -> ProjectPaths:
    return ProjectPaths(project_root=tmp_path / "proj")


def _write_registry(paths: ProjectPaths, *slugs: str) -> None:
    reg = paths.assets_dir / "prop" / "prop.json"
    reg.parent.mkdir(parents=True, exist_ok=True)
    reg.write_text(json.dumps({s: {} for s in slugs}))


def _write_bible(paths: ProjectPaths, props: dict) -> None:
    p = paths.global_bible_path
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(json.dumps({"props": props, "characters": {
        "JADE": {"visual_description": "Lean salvager in a worn flight suit."}}}))


def _write_prop_pool_ref(paths: ProjectPaths, slug: str) -> Path:
    d = paths.pool_dir("prop", slug, "identity", look="base")
    d.mkdir(parents=True, exist_ok=True)
    png = d / f"{slug}_identity_v01.png"
    Image.new("RGB", (8, 8)).save(png)
    return png


def _seg(text: str) -> dict:
    return {"prompt": text, "sublocation": "corridor"}


def _permanent_bible():
    return {_PROP: {"attached_to": "JADE", "description": _DESC,
                    "is_permanent_attachment": True}}


def test_permanent_worn_prop_includes_ref_and_keeps_prose(tmp_path):
    paths = _proj(tmp_path)
    _write_registry(paths, _PROP)
    _write_bible(paths, _permanent_bible())
    png = _write_prop_pool_ref(paths, _PROP)
    refs: list[str] = []
    ref_layout = {"identity_refs": {"JADE": (1, 2)}}
    _append_prop_refs(paths, [_seg("she checks her debt counter")], refs, ref_layout)

    # REC-247: the ref IS now included (permanent + ref + auto-inject default-on)...
    assert str(png) in refs, refs
    assert ref_layout["prop_refs"][_PROP] == len(refs)
    # ...and it is STILL recorded worn, so the carrier prose still emits.
    assert ref_layout["worn_props"]["JADE"] == [_PROP]
    char_descs = _character_descriptions(paths, {"JADE": (1, 2)}, ref_layout["worn_props"])
    assert _DESC in char_descs["JADE"] and "debt counter" in char_descs["JADE"]


def test_permanent_worn_prop_suppress_worn_drops_ref(tmp_path):
    paths = _proj(tmp_path)
    _write_registry(paths, _PROP)
    _write_bible(paths, _permanent_bible())
    _write_prop_pool_ref(paths, _PROP)
    refs: list[str] = []
    ref_layout = {"identity_refs": {"JADE": (1, 2)}}
    _append_prop_refs(
        paths, [_seg("she checks her debt counter")], refs, ref_layout,
        suppress_worn=[_PROP],
    )
    # operator suppressed it → no ref even though it is permanent + has a hero ref
    assert not any(_PROP in r for r in refs), refs
    assert "prop_refs" not in ref_layout


def test_worn_prop_ref_is_labeled_worn_in_prompt():
    # prop present in BOTH prop_refs (ref index) and worn_props (carrier) → worn label.
    ref_layout = {
        "identity_refs": {"JADE": (1, 2)},
        "prop_refs": {_PROP: 3},
        "worn_props": {"JADE": [_PROP]},
    }
    prompt = build_storyboard_strip_prompt(
        [{"shot_id": "S1", "setting": "x"}],
        slots=1,
        char_descs={"JADE": "Lean salvager"},
        ref_layout=ref_layout,
        sublocation_locked=False,
        grid=(1, 1),
    )
    # worn-ref label present; the generic "prop identity" label is NOT used for it
    assert "match its exact appearance" in prompt
    assert "never as a separate free-floating object" in prompt
    assert f"is the {_PROP} prop identity." not in prompt


def test_worn_ref_respects_two_prop_ref_cap(tmp_path):
    """Cross-loop cap: two detected non-worn props fill the 2-prop-ref cap, so an
    auto-injected permanent worn prop does NOT add a 3rd ref (still recorded worn)."""
    paths = _proj(tmp_path)
    _write_registry(paths, "salvage_hook", "warden_drone", _PROP)
    _write_bible(paths, {
        # two NON-worn props (no carrier present) -> each keeps a standalone ref
        "salvage_hook": {"description": "A rusted hook."},
        "warden_drone": {"description": "A patrol drone."},
        # a permanent WORN prop on JADE (auto-injected)
        _PROP: {"attached_to": "JADE", "description": _DESC,
                "is_permanent_attachment": True},
    })
    for s in ("salvage_hook", "warden_drone", _PROP):
        _write_prop_pool_ref(paths, s)
    refs: list[str] = []
    ref_layout = {"identity_refs": {"JADE": (1, 2)}}
    _append_prop_refs(
        paths,
        [_seg("the salvage hook and warden drone clutter the deck")],
        refs, ref_layout,
    )
    # exactly 2 prop refs (the cap); the worn debt_counter ref is NOT a 3rd
    assert len(ref_layout.get("prop_refs", {})) == 2, ref_layout.get("prop_refs")
    assert _PROP not in ref_layout.get("prop_refs", {})
    # ...but it IS still recorded worn for the carrier prose
    assert ref_layout["worn_props"]["JADE"] == [_PROP]
