"""Area #2 W3 — semantic_contract_router (Patch B-min 재진입).

Rule 1 (subject_state SOT) + Rule 2 (character_state element 보존) + null path +
IMMOBILIZED_GAZE regression guard.

W3 fix-up review scope:
- SemanticContract.evidence.source / source_states / primary_mode /
  sanitizer_constraints 모두 unit level verify (downstream prompt_sanitizer.py
  read 의무 키 — semantic_mode / preserve_subject_state /
  forbid_state_polarity_rewrite / override_strategy_prefix / source_states).
- immobilized ⊆ pose_locked invariant (router line 141 `pose_locked.update(immobilized)`).
"""
import pytest

from app.modules.semantic_contract_router import build_semantic_contract
from app.modules import semantic_contract_router as router_module


# ---------------------------- Rule 1 — subject_state (Q5 closure) -------------


@pytest.mark.parametrize("state", ["dead", "unconscious", "severely_injured"])
def test_rule_1_immobilized_state_dispatch(state):
    contract = build_semantic_contract(
        shot_staging={
            "character_angles": [
                {"character": "Alpha", "angle": "back_to_camera", "body_pose": "lying",
                 "gaze_direction_kind": "closed_eyes", "subject_state": state},
            ],
        },
        render_prompt_card=None,
        visible_entities=[{"name": "Alpha", "short_id": "C01", "entity_type": "character"}],
    )
    assert contract.primary_mode == "immobilized"
    assert contract.immobilized_entity_ids == ("C01",)
    # invariant: immobilized ⊆ pose_locked (router line 141)
    assert contract.pose_locked_entity_ids == ("C01",)
    assert contract.source_states == (("C01", state),)
    assert list(contract.evidence) == [{
        "source": "shot_staging.character_angles.subject_state",
        "entity_id": "C01",
        "value": state,
    }]
    sources = [e["source"] for e in contract.evidence]
    assert "shot_staging.character_angles.gaze_target" not in sources
    sc = contract.sanitizer_constraints
    assert sc is not None
    assert sc["semantic_mode"] == "immobilized"
    assert sc["preserve_subject_state"] is True
    assert sc["forbid_state_polarity_rewrite"] is True
    assert sc["override_strategy_prefix"] is True
    assert sc["source_states"] == {"C01": state}
    # downstream prompt_sanitizer.py:101-103 read (user_prompt block 안 True flag 렌더)
    assert sc["preserve_pose"] is True
    assert sc["forbid_unharmed_rewrite"] is True
    assert sc["forbid_active_reaction"] is True
    # downstream prompt_sanitizer.py:141 read (pose_locked branch), router emit always
    assert sc["entity_ids"] == ["C01"]
    # router contract observability (line 53 evidence sanitize copy)
    assert sc["evidence"] == [{
        "source": "shot_staging.character_angles.subject_state",
        "entity_id": "C01",
        "value": state,
    }]


def test_rule_1_skip_alive():
    contract = build_semantic_contract(
        shot_staging={
            "character_angles": [
                {"character": "Gamma", "gaze_direction_kind": "camera", "subject_state": "alive",
                 "angle": "facing_camera", "body_pose": "standing"},
            ],
        },
        render_prompt_card=None,
        visible_entities=[{"name": "Gamma", "short_id": "C03", "entity_type": "character"}],
    )
    assert contract.primary_mode == "none"
    assert contract.immobilized_entity_ids == ()
    assert contract.pose_locked_entity_ids == ()
    assert contract.sanitizer_constraints is None


# ---------------------------- Rule 2 — character_state element ---------------


def test_rule_2_character_state_produces_pose_locked():
    """W3 가 Rule 2 무영향 — Patch B-min 보존 (router line 111-131)."""
    contract = build_semantic_contract(
        shot_staging=None,
        render_prompt_card={
            "continuity_elements_used": {
                "fixed_elements": [
                    {"element_type": "character_state", "character_name": "Beta",
                     "element_id": "S03"},
                ],
            },
        },
        visible_entities=[{"name": "Beta", "short_id": "C02", "entity_type": "character"}],
    )
    assert contract.primary_mode == "pose_locked"
    assert contract.pose_locked_entity_ids == ("C02",)
    assert contract.immobilized_entity_ids == ()
    sources = [e["source"] for e in contract.evidence]
    assert "render_prompt_card.continuity_elements_used.fixed_elements.character_name" in sources
    sc = contract.sanitizer_constraints
    assert sc is not None
    assert sc["semantic_mode"] == "pose_locked"
    # pose_locked branch — sanitizer wording softer (character_state 과탐 방지)
    assert sc["preserve_subject_state"] is False
    assert sc["forbid_state_polarity_rewrite"] is False


# ---------------------------- null / no-signal path --------------------------


@pytest.mark.parametrize("staging", [
    None,
    {},
    {"character_angles": None},
    {"character_angles": []},
])
def test_no_signals_returns_none_mode(staging):
    """Intentional graceful path — router line 96 chain (spec §4.3, Codex iter 7-8 APPROVED)."""
    contract = build_semantic_contract(
        shot_staging=staging, render_prompt_card=None, visible_entities=[],
    )
    assert contract.primary_mode == "none"
    assert contract.immobilized_entity_ids == ()
    assert contract.pose_locked_entity_ids == ()
    assert contract.sanitizer_constraints is None


# ---------------------------- Regression guard --------------------------------


def test_immobilized_gaze_constant_removed():
    """Area #2 W3 — IMMOBILIZED_GAZE deleted; reintroduction = SOT drift to router."""
    assert not hasattr(router_module, "IMMOBILIZED_GAZE")
