from __future__ import annotations

import json

import pytest

from recoil.core.paths import ProjectPaths
from recoil.pipeline._lib import board_builder as bb
from recoil.pipeline._lib.prompt_engine import render_prop_invariant


PROP = "debt_counter"
DESC = "amber readout worn on the left wrist"


def _carrier_bible(**overrides) -> dict:
    prop = {
        "attached_to": "JADE",
        "is_permanent_attachment": True,
        "description": DESC,
    }
    prop.update(overrides)
    return {"props": {PROP: prop}}


def _fact(description: str = DESC) -> dict:
    return {"prop_id": PROP, "carrier": "JADE", "description": description}


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


def _write_bible(paths: ProjectPaths, data: dict) -> None:
    paths.global_bible_path.parent.mkdir(parents=True, exist_ok=True)
    paths.global_bible_path.write_text(json.dumps(data), encoding="utf-8")


def test_structured_prop_derives_fact_when_prose_dropped_prop_name():
    shots = [{"asset_data": {"props": [{"prop_id": PROP}]}}]
    segments = [{"prompt": "Jade checks the amber readout."}]

    assert bb._carrier_facts(_carrier_bible(), shots, segments) == [_fact()]


def test_serialized_canonical_shot_raw_is_unwrapped_for_structured_props():
    shots = [{"asset_data": {"props": []}, "raw": {"asset_data": {"props": [{"prop_id": PROP}]}}}]
    segments = [{"prompt": "Jade checks the amber readout."}]

    assert bb._carrier_facts(_carrier_bible(), shots, segments) == [_fact()]


def test_render_prop_invariant_contains_required_bytes_and_parenthetical():
    rendered = render_prop_invariant(PROP, "JADE", "amber readout...")
    lowered = rendered.lower()

    assert PROP in lowered
    assert "jade" in lowered
    assert "permanent attachment" in lowered
    assert "free-floating" in lowered
    assert "(amber readout...)" in lowered

    rendered_without_description = render_prop_invariant(PROP, "JADE", "")
    lowered_without_description = rendered_without_description.lower()
    assert PROP in lowered_without_description
    assert "jade" in lowered_without_description
    assert "permanent attachment" in lowered_without_description
    assert "free-floating" in lowered_without_description
    assert "(" not in rendered_without_description
    assert ")" not in rendered_without_description


def test_text_path_derives_fact_without_structured_props():
    segments = [{"intent": "Jade taps the debt counter before moving."}]

    assert bb._carrier_facts(_carrier_bible(), [], segments) == [_fact()]


def test_text_path_not_disabled_by_unrelated_structured_prop():
    shots = [{"asset_data": {"props": [{"prop_id": "unrelated_prop"}]}}]
    segments = [{"source_text": "Jade taps the debt counter before moving."}]

    assert bb._carrier_facts(_carrier_bible(), shots, segments) == [_fact()]


def test_carrier_presence_per_shot_characters_derives_fact_case_insensitively():
    shots = [{"asset_data": {"characters": [{"char_id": "jade"}], "props": []}}]
    segments = [{"prompt": "Jade checks the amber readout."}]

    assert bb._carrier_facts(_carrier_bible(), shots, segments) == [_fact()]


def test_carrier_presence_shared_characters_only_derives_fact():
    shots = [{"asset_data": {"characters": [], "props": []}}]
    segments = [{"prompt": "Jade checks the amber readout."}]

    assert bb._carrier_facts(
        _carrier_bible(),
        shots,
        segments,
        shared_characters=["JADE"],
    ) == [_fact()]


def test_text_match_is_whole_token_not_substring():
    assert bb._carrier_facts(
        _carrier_bible(),
        [],
        [{"prompt": "debt countermeasure deployed"}],
    ) == []
    assert bb._carrier_facts(
        _carrier_bible(),
        [],
        [{"prompt": "counterfeit debt"}],
    ) == []


def test_carrier_prop_inactive_when_not_structured_text_or_carrier_present():
    segments = [{"prompt": "Jade checks an amber readout."}]

    assert bb._carrier_facts(_carrier_bible(), [], segments) == []


def test_attached_prop_with_false_permanent_flag_is_not_carrier_bound():
    shots = [{"asset_data": {"props": [{"prop_id": PROP}]}}]

    assert bb._carrier_facts(
        _carrier_bible(is_permanent_attachment=False),
        shots,
        [],
    ) == []


@pytest.mark.parametrize("attached_to", ["", None])
def test_permanent_prop_without_carrier_is_not_carrier_bound(attached_to):
    shots = [{"asset_data": {"props": [{"prop_id": PROP}]}}]

    assert bb._carrier_facts(_carrier_bible(attached_to=attached_to), shots, []) == []


def test_empty_inputs_are_tolerant_and_inactive():
    assert bb._carrier_facts({}, [], []) == []
    assert bb._carrier_facts(_carrier_bible(), [], []) == []


def test_carrier_facts_bible_shape_absent_tolerant_and_corruption_loud():
    shots = [{"asset_data": {"characters": [{"char_id": "JADE"}], "props": []}}]
    segments = [{"prompt": "amber readout"}]

    assert bb._carrier_facts({}, shots, segments) == []
    assert bb._carrier_facts({"characters": {"JADE": {}}}, shots, segments) == []

    with pytest.raises(bb.BoardBuilderError):
        bb._carrier_facts({"props": "not a dict"}, shots, segments)
    with pytest.raises(bb.BoardBuilderError):
        bb._carrier_facts({"props": None}, shots, segments)
    with pytest.raises(bb.BoardBuilderError):
        bb._carrier_facts({"props": {PROP: "a string"}}, shots, segments)

    assert bb._carrier_facts({"props": {PROP: {}}}, shots, segments) == []


@pytest.mark.parametrize("bad_value", ["true", 1])
def test_is_permanent_attachment_type_corruption_fails_closed(bad_value):
    shots = [{"asset_data": {"characters": [{"char_id": "JADE"}], "props": []}}]
    bible = {"props": {PROP: {"attached_to": "JADE", "is_permanent_attachment": bad_value}}}

    with pytest.raises(bb.BoardBuilderError):
        bb._carrier_facts(bible, shots, [])


def test_valid_bool_true_derives_and_absent_permanent_flag_is_not_corrupt():
    shots = [{"asset_data": {"characters": [{"char_id": "JADE"}], "props": []}}]

    assert bb._carrier_facts(_carrier_bible(is_permanent_attachment=True), shots, []) == [_fact()]
    assert bb._carrier_facts(
        {"props": {PROP: {"attached_to": "JADE", "description": DESC}}},
        shots,
        [],
    ) == []


def test_reused_bible_loaders_preserve_strict_and_fail_soft_contracts(tmp_path):
    paths = _project_paths(tmp_path)

    assert bb._load_bible_strict(paths) == {}
    assert bb._load_bible(paths) == {}

    paths.global_bible_path.parent.mkdir(parents=True, exist_ok=True)
    paths.global_bible_path.write_text("{ not json", encoding="utf-8")
    with pytest.raises(bb.BoardBuilderError):
        bb._load_bible_strict(paths)
    assert bb._load_bible(paths) == {}

    _write_bible(paths, {})
    assert bb._load_bible_strict(paths) == {}
    assert bb._load_bible(paths) == {}

    valid = {"props": {PROP: {"attached_to": "JADE", "description": DESC}}}
    _write_bible(paths, valid)
    assert bb._load_bible_strict(paths) == valid
    assert bb._load_bible(paths) == valid

    for corrupt in (
        {"props": "not a dict"},
        {"props": None},
        {"props": {"x": "str"}},
        {"props": {"x": {"attached_to": 5}}},
    ):
        _write_bible(paths, corrupt)
        with pytest.raises(bb.BoardBuilderError):
            bb._load_bible_strict(paths)

    well_formed_missing_optional_keys = {"props": {"x": {}}}
    _write_bible(paths, well_formed_missing_optional_keys)
    assert bb._load_bible_strict(paths) == well_formed_missing_optional_keys


def test_carrier_derivation_is_independent_of_worn_prop_auto_inject_policy():
    # _carrier_facts is intentionally pure over bible, shots, segments, and
    # shared_characters; suppress_worn/auto-inject config cannot disable it.
    shots = [{"asset_data": {"characters": [{"char_id": "JADE"}], "props": []}}]

    assert bb._carrier_facts(_carrier_bible(), shots, []) == [_fact()]
