"""Tests for vision API failure routing to ERROR outcome.

Each critic that calls validate_image must surface API errors as a
real exception so CriticLoop catches it and returns Outcome.ERROR.
The current code records "Skipped" with passed=True, which is silent bypass.
"""

import pytest
from unittest.mock import patch
from recoil.core.critic import Outcome  # via shim — see Task 1 note about import paths


@pytest.fixture
def fake_vision_error():
    """Patch validate_image to return an error response."""

    def _fake(*args, **kwargs):
        return {"error": "vision API timeout (test)", "results": []}

    return _fake


def test_ref_image_critic_returns_error_on_vision_failure(tmp_path, fake_vision_error):
    """RefImageCritic must return Outcome.ERROR on vision API failure, not silent pass."""
    from recoil.pipeline._lib.critics.ref_image_critic import RefImageCritic

    img = tmp_path / "fake.png"
    img.write_bytes(b"fake")  # any bytes, won't be checked

    critic = RefImageCritic(character_type="human")
    with patch(
        "recoil.pipeline._lib.critics.ref_image_critic.validate_image",
        side_effect=fake_vision_error,
    ):
        _, result = critic.run(img)

    assert result.outcome == Outcome.ERROR
    assert result.passed is False


def test_start_frame_critic_returns_error_on_vision_failure(
    tmp_path, fake_vision_error
):
    from recoil.pipeline._lib.critics.start_frame_critic import StartFrameCritic

    img = tmp_path / "frame.png"
    img.write_bytes(b"fake")

    critic = StartFrameCritic(expected_background="scene")
    with patch(
        "recoil.pipeline._lib.critics.start_frame_critic.validate_image",
        side_effect=fake_vision_error,
    ):
        _, result = critic.run(img)

    assert result.outcome == Outcome.ERROR


def test_video_frame_critic_returns_error_on_vision_failure(
    tmp_path, fake_vision_error
):
    from recoil.pipeline._lib.critics.video_frame_critic import VideoFrameCritic

    vid = tmp_path / "v.mp4"
    vid.write_bytes(b"fake")

    critic = VideoFrameCritic(character_type="human")
    with patch(
        "recoil.pipeline._lib.critics.video_frame_critic.validate_video_frames",
        side_effect=fake_vision_error,
    ):
        _, result = critic.run(vid)

    assert result.outcome == Outcome.ERROR


def test_turnaround_critic_grid_returns_error_on_vision_failure(
    tmp_path, fake_vision_error
):
    from recoil.pipeline._lib.critics.turnaround_critic import TurnaroundCritic

    grid = tmp_path / "grid.png"
    grid.write_bytes(b"fake")

    critic = TurnaroundCritic()
    with patch(
        "recoil.pipeline._lib.critics.turnaround_critic.validate_image",
        side_effect=fake_vision_error,
    ):
        _, result = critic.run(grid)

    assert result.outcome == Outcome.ERROR


def test_no_frames_extracted_is_hard_failure(tmp_path):
    """If video frame extraction returns empty results, treat as HARD fail (corrupted video)."""
    from recoil.pipeline._lib.critics.video_frame_critic import VideoFrameCritic

    vid = tmp_path / "v.mp4"
    vid.write_bytes(b"fake")

    critic = VideoFrameCritic(character_type="human")
    with patch(
        "recoil.pipeline._lib.critics.video_frame_critic.validate_video_frames",
        return_value={"frame_results": []},
    ):
        _, result = critic.run(vid)

    # Empty frame extraction is a real failure, not silent pass
    assert result.outcome == Outcome.FAIL
    assert any(d.name == "FRAME_EXTRACTION" for d in result.failed_dimensions)


def test_turnaround_empty_answer_does_not_silently_pass(tmp_path):
    """Empty Gemini answer on HARD check must NOT be treated as PASS."""
    from recoil.pipeline._lib.critics.turnaround_critic import TurnaroundCritic
    from recoil.core.critic import Outcome

    grid = tmp_path / "g.png"
    grid.write_bytes(b"fake")
    panel = tmp_path / "p.png"
    panel.write_bytes(b"fake")

    fake_grid_result = {
        "results": [
            {
                "name": "FOUR_DISTINCT_PANELS",
                "passed": True,
                "answer": "yes",
                "expected": "yes",
                "severity": "hard",
            },
            {
                "name": "PANEL_ANGLES_CORRECT",
                "passed": True,
                "answer": "yes",
                "expected": "yes",
                "severity": "hard",
            },
            {
                "name": "POSE_REPETITION",
                "passed": True,
                "answer": "yes",
                "expected": "yes",
                "severity": "hard",
            },
        ]
    }
    # Panel result with EMPTY answer on HARD wardrobe check
    fake_panel_result = {
        "results": [
            {
                "name": "WARDROBE_MATCH",
                "passed": False,
                "answer": "",
                "expected": "yes",
                "severity": "hard",
            },
        ]
    }

    critic = TurnaroundCritic(wardrobe="red leather jacket", framing="medium")
    with patch(
        "recoil.pipeline._lib.critics.turnaround_critic.validate_image",
        side_effect=[fake_grid_result, fake_panel_result],
    ):
        _, result = critic.run(grid, context={"panel_paths": [panel]})

    # Empty answer should NOT silently pass — outcome is FAIL or there's an UNDECIDED dim
    assert result.outcome in (Outcome.FAIL, Outcome.ERROR) or any(
        "undecided" in (d.message or "").lower() or not d.passed
        for d in result.dimensions
        if "WARDROBE" in d.name
    )
