"""Each critic returns a FailureMode-typed failure_mode field.

Phase 25 (T1.16) — structured output stubs for the critic layer.

This test is intentionally LENIENT. Full Gemini response_schema integration
is deferred to Phase 2+, so critics that don't yet expose a
``structured_output=True`` path are ``pytest.skip``-ped instead of failed.
The non-negotiable piece is that ``recoil.pipeline._lib.critics.FailureMode`` exists and has
stable string values — everything else is opportunistic.
"""

import importlib
import sys
from pathlib import Path

import pytest

# Ensure the pipeline root is on sys.path (matches conftest.py convention).
PIPELINE_ROOT = Path(__file__).resolve().parent.parent
if str(PIPELINE_ROOT) not in sys.path:
    sys.path.insert(0, str(PIPELINE_ROOT))

from recoil.pipeline._lib.critics import FailureMode  # noqa: E402


def _collect_critics() -> list[str]:
    """Return the stem names of every non-dunder module under _lib/critics/."""
    critics_dir = PIPELINE_ROOT / "_lib" / "critics"
    names: list[str] = []
    for py in sorted(critics_dir.glob("*.py")):
        if py.name.startswith("_"):
            continue
        names.append(py.stem)
    return names


CRITICS: list[str] = _collect_critics()


def test_failure_mode_enum_exists():
    """FailureMode enum must exist and have the expected stable values."""
    assert issubclass(FailureMode, str)
    expected = {
        "none",
        "anatomy_face_merge",
        "anatomy_limb_miscount",
        "identity_drift",
        "background_contamination",
        "wardrobe_mismatch",
        "lighting_mismatch",
        "grid_influence",
        "safety_softened",
        "unknown",
    }
    actual = {m.value for m in FailureMode}
    assert expected.issubset(actual), f"Missing enum values: {expected - actual}"


def test_critics_collected():
    """Sanity: we found at least one critic module."""
    assert CRITICS, "No critic modules discovered in _lib/critics/"


@pytest.mark.parametrize("critic_name", CRITICS)
def test_critic_module_imports(critic_name):
    """Every critic module under lib/critics/ must be importable."""
    importlib.import_module(f"recoil.pipeline._lib.critics.{critic_name}")


@pytest.mark.parametrize("critic_name", CRITICS)
def test_critic_returns_failure_mode_enum_if_structured_output_supported(critic_name):
    """Each critic, when called with ``structured_output=True``, returns a dict
    containing a ``failure_mode`` that is a FailureMode value.

    Skips critics that don't yet expose a structured_output path.
    """
    module = importlib.import_module(f"recoil.pipeline._lib.critics.{critic_name}")

    # Find the critic function. By convention it matches one of:
    #   - {module_name}                  (e.g. anatomy_critic)
    #   - {module_name}_critic           (e.g. anatomy_critic_critic)
    #   - check_{module_name}            (e.g. check_anatomy_critic)
    #   - check_{module_name_sans_suffix}
    candidates = [
        critic_name,
        f"{critic_name}_critic",
        f"check_{critic_name}",
    ]
    if critic_name.endswith("_critic"):
        stem = critic_name[: -len("_critic")]
        candidates.extend(
            [
                stem,
                f"check_{stem}",
            ]
        )

    fn = None
    fn_name = None
    for name in candidates:
        candidate = getattr(module, name, None)
        if callable(candidate):
            fn = candidate
            fn_name = name
            break

    if fn is None:
        pytest.skip(f"no primary critic function found in {critic_name}")

    try:
        result = fn(image=None, reference=None, structured_output=True)
    except TypeError as e:
        if "structured_output" in str(e):
            pytest.skip(
                f"{fn_name} does not accept structured_output kwarg yet — Phase 2 work"
            )
        # Some critics may not accept image/reference kwargs — try positional None
        try:
            result = fn(None, None, structured_output=True)
        except Exception as e2:
            pytest.skip(
                f"{fn_name} signature incompatible — Phase 2 work: "
                f"{type(e2).__name__}: {e2}"
            )
    except Exception as e:
        pytest.skip(
            f"{fn_name} raised {type(e).__name__} with mock inputs — Phase 2 work: {e}"
        )

    assert isinstance(result, dict), (
        f"{fn_name} returned {type(result).__name__}, expected dict"
    )
    assert "failure_mode" in result, f"{fn_name} missing failure_mode in result"
    fm = result["failure_mode"]
    assert isinstance(fm, (FailureMode, str)), f"failure_mode is {type(fm).__name__}"
    if isinstance(fm, str):
        valid_values = [m.value for m in FailureMode]
        assert fm in valid_values, (
            f"failure_mode '{fm}' not in enum values {valid_values}"
        )
