"""Tests for Phase 3 data models: CoveragePassContext, StopOnReview, OpResult, EpisodeResult."""
import pytest
from pathlib import Path


def test_stop_on_review_enum_has_3_values():
    from recoil.pipeline._lib.coverage_context import StopOnReview
    assert len(StopOnReview) == 3
    assert StopOnReview.NEVER.value == "never"
    assert StopOnReview.ON_HARD_FAIL.value == "on_hard_fail"
    assert StopOnReview.ON_ANY_REVIEW.value == "on_any_review"


def test_terminal_statuses_has_7_values():
    from recoil.pipeline._lib.coverage_context import TERMINAL_STATUSES
    assert len(TERMINAL_STATUSES) == 7
    expected = {
        "ok", "budget_exhausted_success", "needs_review",
        "budget_exhausted", "attempts_exhausted", "icu_escalated", "crashed",
    }
    assert TERMINAL_STATUSES == expected


def test_skip_and_retry_on_resume_partition_statuses():
    """SKIP_ON_RESUME and RETRY_ON_RESUME must be disjoint and cover all terminal statuses."""
    from recoil.pipeline._lib.coverage_context import TERMINAL_STATUSES, SKIP_ON_RESUME, RETRY_ON_RESUME
    assert SKIP_ON_RESUME & RETRY_ON_RESUME == set()
    assert SKIP_ON_RESUME | RETRY_ON_RESUME == TERMINAL_STATUSES


def test_op_result_valid_status():
    from recoil.pipeline._lib.coverage_context import OpResult
    result = OpResult(status="ok", shot_id="EP001_SH01", op_id="op_test123456")
    assert result.status == "ok"
    assert result.cost_usd == 0.0
    assert result.validation_notes == []


def test_op_result_rejects_invalid_status():
    from recoil.pipeline._lib.coverage_context import OpResult
    with pytest.raises(ValueError, match="must be one of"):
        OpResult(status="bogus", shot_id="x", op_id="op_test")


def test_op_result_all_7_statuses_accepted():
    from recoil.pipeline._lib.coverage_context import OpResult, TERMINAL_STATUSES
    for status in TERMINAL_STATUSES:
        result = OpResult(status=status, shot_id="SH01", op_id="op_abc")
        assert result.status == status


def test_coverage_pass_context_to_dict():
    from recoil.pipeline._lib.coverage_context import CoveragePassContext
    ctx = CoveragePassContext(
        coverage_pass_id="SC01_A",
        sibling_shot_ids=["SH01", "SH02"],
        this_shot_role="primary",
        completed_siblings={"SH02": "ok"},
        pass_min_success=1,
    )
    d = ctx.to_dict()
    assert d["coverage_pass_id"] == "SC01_A"
    assert d["sibling_shot_ids"] == ["SH01", "SH02"]
    assert d["completed_siblings"] == {"SH02": "ok"}
    assert d["pass_min_success"] == 1


def test_coverage_pass_context_to_dict_is_independent_copy():
    """to_dict() must return independent copies, not references."""
    from recoil.pipeline._lib.coverage_context import CoveragePassContext
    siblings = {"SH02": "ok"}
    ctx = CoveragePassContext(
        coverage_pass_id="SC01_A",
        sibling_shot_ids=["SH01", "SH02"],
        this_shot_role="primary",
        completed_siblings=siblings,
    )
    d = ctx.to_dict()
    d["completed_siblings"]["SH03"] = "crashed"
    # Original must not be affected
    assert "SH03" not in ctx.completed_siblings


def test_episode_result_morning_summary():
    from recoil.pipeline._lib.coverage_context import EpisodeResult, OpResult
    er = EpisodeResult(
        run_id="run_001",
        episode_id="EP001",
        total_shots=10,
        completed=8,
        by_status={"ok": 6, "needs_review": 2},
        total_cost_usd=4.50,
        budget_remaining_usd=45.50,
        review_queue_count=2,
        shot_results=[
            OpResult(status="needs_review", shot_id="SH03", op_id="op_a",
                     validation_notes=["soft identity drift"]),
            OpResult(status="needs_review", shot_id="SH07", op_id="op_b",
                     validation_notes=["background contamination"]),
        ],
    )
    summary = er.morning_summary()
    assert "EP001" in summary
    assert "8/10" in summary
    assert "$4.50" in summary
    assert "needs_review: 2" in summary
    assert "soft identity drift" in summary
    assert "Review queue: 2" in summary


def test_episode_result_morning_summary_aborted():
    from recoil.pipeline._lib.coverage_context import EpisodeResult
    er = EpisodeResult(
        run_id="run_002",
        episode_id="EP002",
        aborted=True,
        abort_reason="budget exhausted",
    )
    summary = er.morning_summary()
    assert "ABORTED" in summary
    assert "budget exhausted" in summary


def test_op_result_serialization_fields():
    """OpResult stores all expected fields."""
    from recoil.pipeline._lib.coverage_context import OpResult
    result = OpResult(
        status="needs_review",
        shot_id="EP001_SH03",
        op_id="op_abc123def4",
        output_path="/tmp/frame.png",
        cost_usd=0.134,
        attempts=3,
        failure_mode="anatomy_face_merge",
        validation_notes=["extra limb detected"],
        review_queue_id="rq_xyz789",
        coverage_context={"coverage_pass_id": "SC01_A"},
    )
    assert result.output_path == "/tmp/frame.png"
    assert result.failure_mode == "anatomy_face_merge"
    assert result.review_queue_id == "rq_xyz789"
