"""Phase 3.5 Cleanup Pass -- Acceptance Tests.

Tests all 5 items:
1. Taxonomy Unification (FailureMode on Dimension)
2. Per-Model Critic Tuning (critic_overrides)
3. Sibling Pixel Refs (get_approved_neighbors)
4. Layer Patches Killed (FeedbackFix has no layer_patches)
5. Per-Scene Style Anchors (EpisodeResult.style_anchors dict)
"""

import sys
from pathlib import Path
from unittest.mock import MagicMock
from dataclasses import fields as dataclass_fields


# Ensure project root is on path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))


# =====================================================================
# 1. Taxonomy Unification
# =====================================================================


class TestTaxonomyUnification:
    """FailureMode lives in core.critic, Dimension has failure_mode field."""

    def test_failuremode_importable_from_core(self):
        from recoil.core.critic import FailureMode

        assert hasattr(FailureMode, "ANATOMY_FACE_MERGE")
        assert FailureMode.ANATOMY_FACE_MERGE.value == "anatomy_face_merge"

    def test_failuremode_reexported_from_critics_init(self):
        from recoil.pipeline._lib.critics import FailureMode
        from recoil.core.critic import FailureMode as CoreFM

        assert FailureMode is CoreFM

    def test_dimension_has_failure_mode_field(self):
        from recoil.core.critic import Dimension, Severity, FailureMode

        dim = Dimension(
            name="TEST",
            severity=Severity.HARD,
            passed=False,
            message="test failure",
            failure_mode=FailureMode.ANATOMY_FACE_MERGE,
        )
        assert dim.failure_mode == FailureMode.ANATOMY_FACE_MERGE

    def test_dimension_failure_mode_defaults_to_none(self):
        from recoil.core.critic import Dimension, Severity

        dim = Dimension(name="TEST", severity=Severity.SOFT)
        assert dim.failure_mode is None

    def test_critic_result_dominant_failure_mode(self):
        from recoil.core.critic import CriticResult, Dimension, Severity, FailureMode, Outcome

        result = CriticResult(
            critic_name="test",
            outcome=Outcome.FAIL,
            dimensions=[
                Dimension(name="A", severity=Severity.SOFT, passed=True),
                Dimension(
                    name="B",
                    severity=Severity.HARD,
                    passed=False,
                    message="drift",
                    failure_mode=FailureMode.IDENTITY_DRIFT,
                ),
                Dimension(
                    name="C",
                    severity=Severity.HARD,
                    passed=False,
                    message="anatomy",
                    failure_mode=FailureMode.ANATOMY_FACE_MERGE,
                ),
            ],
        )
        assert result.dominant_failure_mode == FailureMode.IDENTITY_DRIFT

    def test_critic_result_dominant_failure_mode_none_when_passing(self):
        from recoil.core.critic import CriticResult, Dimension, Severity, Outcome

        result = CriticResult(
            critic_name="test",
            outcome=Outcome.PASS,
            dimensions=[
                Dimension(name="A", severity=Severity.HARD, passed=True),
            ],
        )
        assert result.dominant_failure_mode is None

    def test_to_dict_serializes_failure_mode(self):
        from recoil.core.critic import CriticResult, Dimension, Severity, FailureMode, Outcome

        result = CriticResult(
            critic_name="test",
            outcome=Outcome.FAIL,
            dimensions=[
                Dimension(
                    name="B",
                    severity=Severity.HARD,
                    passed=False,
                    failure_mode=FailureMode.IDENTITY_DRIFT,
                ),
            ],
        )
        d = result.to_dict()
        assert d["dimensions"][0]["failure_mode"] == "identity_drift"

    def test_all_failure_modes_present(self):
        """Verify all 23 FailureMode values are defined."""
        from recoil.core.critic import FailureMode

        expected = {
            "none",
            "anatomy_face_merge",
            "anatomy_limb_miscount",
            "identity_drift",
            "background_contamination",
            "wardrobe_mismatch",
            "lighting_mismatch",
            "grid_influence",
            "safety_softened",
            "unknown",
            "motion_failure",
            "end_frame_drift",
            "content_filter_hard_block",
            "ref_bleed",
            "audio_sync_drift",
            "coverage_geometry_broken",
            "composition_wrong",
            "cost_overrun",
            "cuts_too_soft",
            "gate_mechanical",
            "prompt_duration_mismatch",
            "style_drift",
            "transient",
        }
        actual = {fm.value for fm in FailureMode}
        assert actual == expected


# =====================================================================
# 2. Per-Model Critic Tuning
# =====================================================================


class TestPerModelCriticTuning:
    """critic_overrides in model_profiles.json + get_critic_override helper."""

    def test_get_critic_override_returns_configured_value(self):
        from recoil.core.model_profiles import get_critic_override, reload

        reload()
        val = get_critic_override("seedream-v4.5", "anatomy", "strictness", "standard")
        assert val == "relaxed"

    def test_get_critic_override_returns_default_for_missing_critic(self):
        from recoil.core.model_profiles import get_critic_override, reload

        reload()
        val = get_critic_override(
            "seedream-v4.5", "nonexistent_critic", "strictness", "standard"
        )
        assert val == "standard"

    def test_get_critic_override_returns_default_for_missing_model(self):
        from recoil.core.model_profiles import get_critic_override

        val = get_critic_override(
            "nonexistent-model", "anatomy", "strictness", "standard"
        )
        assert val == "standard"

    def test_kling_v3_anatomy_is_strict(self):
        from recoil.core.model_profiles import get_critic_override, reload

        reload()
        val = get_critic_override("kling-v3", "anatomy", "strictness", "standard")
        assert val == "strict"

    def test_model_profiles_json_schema_valid(self):
        """critic_overrides entries have valid strictness values."""
        from recoil.core.model_profiles import _load, reload

        reload()
        profiles = _load()
        valid_levels = {"strict", "standard", "relaxed"}
        from recoil.core.model_profiles import iter_model_ids
        for model_id in iter_model_ids(profiles):
            profile = profiles[model_id]
            overrides = profile.get("critic_overrides", {})
            for critic_name, cfg in overrides.items():
                strictness = cfg.get("strictness")
                if strictness is not None:
                    assert strictness in valid_levels, (
                        f"{model_id}.critic_overrides.{critic_name}.strictness "
                        f"= '{strictness}' not in {valid_levels}"
                    )


# =====================================================================
# 3. Sibling Pixel Refs
# =====================================================================


class TestSiblingPixelRefs:
    """ExecutionStore.get_approved_neighbors() + run_shot wiring."""

    def test_get_approved_neighbors_returns_previous(self, tmp_path):
        from recoil.execution.execution_store import ExecutionStore

        store = ExecutionStore(project="test", db_path=tmp_path / "shots")
        store.insert_shot(
            {
                "shot_id": "EP001_SH01",
                "episode_id": "EP001",
                "status": "approved",
                "output_path": "/tmp/sh01.jpg",
            }
        )
        store.insert_shot(
            {
                "shot_id": "EP001_SH02",
                "episode_id": "EP001",
                "status": "keyframe_generating",
            }
        )
        store.insert_shot(
            {
                "shot_id": "EP001_SH03",
                "episode_id": "EP001",
                "status": "approved",
                "output_path": "/tmp/sh03.jpg",
            }
        )

        neighbors = store.get_approved_neighbors("EP001_SH02", "EP001")
        assert neighbors["previous"] is not None
        assert neighbors["previous"]["shot_id"] == "EP001_SH01"
        assert neighbors["previous"]["output_path"] == "/tmp/sh01.jpg"

    def test_get_approved_neighbors_no_previous(self, tmp_path):
        from recoil.execution.execution_store import ExecutionStore

        store = ExecutionStore(project="test", db_path=tmp_path / "shots")
        store.insert_shot(
            {
                "shot_id": "EP001_SH01",
                "episode_id": "EP001",
                "status": "keyframe_generating",
            }
        )

        neighbors = store.get_approved_neighbors("EP001_SH01", "EP001")
        assert neighbors["previous"] is None

    def test_get_approved_neighbors_returns_next(self, tmp_path):
        from recoil.execution.execution_store import ExecutionStore

        store = ExecutionStore(project="test", db_path=tmp_path / "shots")
        store.insert_shot(
            {
                "shot_id": "EP001_SH01",
                "episode_id": "EP001",
                "status": "keyframe_generating",
            }
        )
        store.insert_shot(
            {
                "shot_id": "EP001_SH02",
                "episode_id": "EP001",
                "status": "video_complete",
                "output_path": "/tmp/sh02.mp4",
            }
        )

        neighbors = store.get_approved_neighbors("EP001_SH01", "EP001")
        assert neighbors["next"] is not None
        assert neighbors["next"]["shot_id"] == "EP001_SH02"

    def test_get_approved_neighbors_unknown_shot(self, tmp_path):
        from recoil.execution.execution_store import ExecutionStore

        store = ExecutionStore(project="test", db_path=tmp_path / "shots")
        neighbors = store.get_approved_neighbors("NONEXISTENT", "EP001")
        assert neighbors["previous"] is None
        assert neighbors["next"] is None


# =====================================================================
# 4. Layer Patches Killed
# =====================================================================


class TestLayerPatchesKilled:
    """FeedbackFix and FeedbackAttempt no longer have layer_patches."""

    def test_feedbackfix_has_no_layer_patches_field(self):
        from recoil.execution.feedback.agent import FeedbackFix

        field_names = {f.name for f in dataclass_fields(FeedbackFix)}
        assert "layer_patches" not in field_names

    def test_feedbackfix_has_no_apply_layer_patches_method(self):
        from recoil.execution.feedback.agent import FeedbackFix

        assert not hasattr(FeedbackFix, "apply_layer_patches")

    def test_feedbackattempt_has_no_layer_patched_field(self):
        from recoil.execution.feedback.agent import FeedbackAttempt

        field_names = {f.name for f in dataclass_fields(FeedbackAttempt)}
        assert "layer_patched" not in field_names

    def test_fix_registry_entries_have_no_layer_patches(self):
        from recoil.execution.feedback.fix_registry import FIX_REGISTRY

        for i, entry in enumerate(FIX_REGISTRY):
            assert "layer_patches" not in entry, (
                f"FIX_REGISTRY[{i}] ({entry.get('category')}) still has layer_patches"
            )

    def test_feedbackfix_constructs_without_layer_patches(self):
        from recoil.execution.feedback.agent import FeedbackFix, FeedbackStrategy

        fix = FeedbackFix(
            strategy=FeedbackStrategy.SEED_REROLL,
            ref_changes=None,
            negative_prompt_additions=[],
            confidence=0.5,
            rationale="test",
            diagnosis_cost=0.0,
        )
        assert fix.strategy == FeedbackStrategy.SEED_REROLL


# =====================================================================
# 5. Per-Scene Style Anchors
# =====================================================================


class TestPerSceneStyleAnchors:
    """EpisodeResult.style_anchors dict + scene grouping."""

    def test_episode_result_has_style_anchors_dict(self):
        from recoil.pipeline._lib.coverage_context import EpisodeResult

        er = EpisodeResult(run_id="r1", episode_id="EP001")
        assert isinstance(er.style_anchors, dict)
        assert len(er.style_anchors) == 0

    def test_episode_result_style_anchor_path_compat(self):
        """Backward-compat property returns first anchor."""
        from recoil.pipeline._lib.coverage_context import EpisodeResult

        er = EpisodeResult(
            run_id="r1",
            episode_id="EP001",
            style_anchors={
                "SC01": Path("/tmp/anchor1.jpg"),
                "SC02": Path("/tmp/anchor2.jpg"),
            },
        )
        assert er.style_anchor_path == Path("/tmp/anchor1.jpg")

    def test_episode_result_style_anchor_path_none_when_empty(self):
        from recoil.pipeline._lib.coverage_context import EpisodeResult

        er = EpisodeResult(run_id="r1", episode_id="EP001")
        assert er.style_anchor_path is None

    def test_get_style_anchor_for_scene(self):
        from recoil.pipeline._lib.coverage_context import EpisodeResult

        er = EpisodeResult(
            run_id="r1",
            episode_id="EP001",
            style_anchors={
                "SC01": Path("/tmp/sc01.jpg"),
                "SC02": Path("/tmp/sc02.jpg"),
                "episode": Path("/tmp/ep.jpg"),
            },
        )
        assert er.get_style_anchor_for_scene("SC01") == Path("/tmp/sc01.jpg")
        assert er.get_style_anchor_for_scene("SC02") == Path("/tmp/sc02.jpg")
        assert er.get_style_anchor_for_scene("SC99") == Path("/tmp/ep.jpg")

    def test_scene_grouping(self):
        """Shots group by scene_index, then location, then 'episode'."""
        from recoil.pipeline._lib.run_episode import _group_shots_by_scene

        shots = [
            {"shot_id": "SH01", "scene_index": "SC01"},
            {"shot_id": "SH02", "scene_index": "SC01"},
            {"shot_id": "SH03", "scene_index": "SC02"},
            {"shot_id": "SH04", "location": "bar"},
            {"shot_id": "SH05"},
        ]
        groups = _group_shots_by_scene(shots)
        assert "SC01" in groups
        assert len(groups["SC01"]) == 2
        assert "SC02" in groups
        assert len(groups["SC02"]) == 1
        assert "bar" in groups
        assert len(groups["bar"]) == 1
        assert "episode" in groups
        assert len(groups["episode"]) == 1

    def test_morning_summary_shows_style_anchors(self):
        from recoil.pipeline._lib.coverage_context import EpisodeResult

        er = EpisodeResult(
            run_id="r1",
            episode_id="EP001",
            total_shots=10,
            completed=10,
            style_anchors={"SC01": Path("/a.jpg"), "SC02": Path("/b.jpg")},
        )
        summary = er.morning_summary()
        assert "Style anchors: 2" in summary
        assert "SC01" in summary


# =====================================================================
# Integration: Taxonomy + Action Routing
# =====================================================================


class TestTaxonomyActionRouting:
    """_extract_failure_mode reads typed Dimension.failure_mode."""

    def test_extract_failure_mode_from_typed_dimension(self):
        from recoil.pipeline._lib.run_shot import _extract_failure_mode
        from recoil.core.critic import CriticResult, Dimension, Severity, FailureMode, Outcome

        critic_result = CriticResult(
            critic_name="ref_image",
            outcome=Outcome.FAIL,
            dimensions=[
                Dimension(
                    name="EXTRA_APPENDAGES",
                    severity=Severity.HARD,
                    passed=False,
                    message="phantom arm",
                    failure_mode=FailureMode.ANATOMY_LIMB_MISCOUNT,
                ),
            ],
        )
        step_result = MagicMock()
        step_result.error = ""
        step_result.final_state = ""
        step_result.gate_verdict = MagicMock()
        step_result.gate_verdict.passed = False
        step_result.gate_verdict.gate_name = "gate_1"
        step_result.gate_verdict.details = {
            "critic_result": critic_result,
        }

        fm = _extract_failure_mode(step_result)
        assert fm == FailureMode.ANATOMY_LIMB_MISCOUNT

    def test_extract_failure_mode_falls_back_to_string_matching(self):
        """Legacy path: no critic_result in details, uses string matching."""
        from recoil.pipeline._lib.run_shot import _extract_failure_mode
        from recoil.core.critic import FailureMode

        step_result = MagicMock()
        step_result.error = ""
        step_result.final_state = ""
        step_result.gate_verdict = MagicMock()
        step_result.gate_verdict.passed = False
        step_result.gate_verdict.gate_name = "gate_2a_identity"
        step_result.gate_verdict.details = {"failure_category": ""}

        fm = _extract_failure_mode(step_result)
        assert fm == FailureMode.IDENTITY_DRIFT
