"""Phase 3 (CP-3) test — verify BUILDERS dispatch + get_builder() lookup.

Phase 3 wired nbp/keyframe, flash/previz, and the gemini-* image
variants into BUILDERS. The strict strategy-snapshot superset test
(test_keys_match_provider_strategy_snapshot) is now ENFORCING — every
model_id in provider_strategy.json must have at least one BUILDERS entry.
"""

from __future__ import annotations

import sys
from pathlib import Path

import pytest

# Bootstrap (in case test runs outside the installed venv).
_RECOIL_ROOT = Path(__file__).resolve().parents[3]
if str(_RECOIL_ROOT) not in sys.path:
    sys.path.insert(0, str(_RECOIL_ROOT))
from recoil.core.paths import ensure_pipeline_importable  # noqa: E402
ensure_pipeline_importable()

from recoil.pipeline._lib.prompt_engine import (  # noqa: E402
    BUILDERS,
    get_builder,
    build_kling_i2v_prompt,
    build_seeddance_r2v_prompt,
    build_wan_i2v_prompt,
)


# Models with builders registered after Phase 3. This is the full set of
# 12 model_ids in provider_strategy.json — Phase 3 wired the remaining
# 5 (nbp, flash, gemini-2.5-flash-image, gemini-3-pro-image-preview,
# gemini-3.1-flash-image-preview) on top of Phase 2's 7.
PHASE_3_COVERED_MODELS = {
    "kling-o3",
    "kling-v3",
    "kling-v3-i2v",
    "seeddance-2.0",
    "wan-2.7-i2v",
    "wan-2.7-r2v",
    "veo-3.1",
    "nbp",
    "flash",
    "gemini-2.5-flash-image",
    "gemini-3-pro-image-preview",
    "gemini-3.1-flash-image-preview",
}


def test_dispatch_table_non_empty():
    assert len(BUILDERS) >= 10, f"BUILDERS table too small: {len(BUILDERS)} entries"


def test_kling_o3_i2v_resolves():
    fn = get_builder("kling-o3", "i2v")
    assert fn is build_kling_i2v_prompt


def test_seeddance_r2v_resolves():
    fn = get_builder("seeddance-2.0", "r2v")
    assert fn is build_seeddance_r2v_prompt


def test_wan_i2v_resolves():
    fn = get_builder("wan-2.7-i2v", "i2v")
    assert fn is build_wan_i2v_prompt


def test_missing_modality_raises_keyerror_with_supported_list():
    with pytest.raises(KeyError) as exc:
        get_builder("kling-o3", "purple")
    msg = str(exc.value)
    assert "kling-o3" in msg and "Supported modalities" in msg


def test_missing_model_raises_keyerror_with_helper():
    with pytest.raises(KeyError) as exc:
        get_builder("imagined-future-model-v99", "i2v")
    msg = str(exc.value)
    assert "imagined-future-model-v99" in msg and "not registered" in msg


def test_phase_3_covered_models_present():
    """Every Phase-3-covered model_id (full strategy set) appears in BUILDERS."""
    builders_models = {mid for (mid, _) in BUILDERS}
    missing = PHASE_3_COVERED_MODELS - builders_models
    assert not missing, f"BUILDERS missing Phase 3 model_ids: {missing}"


def test_builders_identity_preserved():
    """Every BUILDERS entry maps to a real top-level function in lib.prompt_engine."""
    import recoil.pipeline._lib.prompt_engine as pe
    for (mid, mod), fn in BUILDERS.items():
        direct = getattr(pe, fn.__name__, None)
        assert direct is fn, f"BUILDERS[{mid},{mod}] = {fn.__name__} drift"


def test_keys_match_provider_strategy_snapshot():
    """Every visual-pipeline model_id in provider_strategy.json must appear in BUILDERS.

    Strict superset check — re-tightened in Phase 3 (CP-3) after the
    previously deferred nbp/keyframe + flash/previz + gemini-* entries
    were wired into BUILDERS. (The @pytest.mark.xfail decorator from
    Phase 2 has been removed.)

    CP-8 (Phase 3) added audio_t2a + lipsync_post entries (eleven_multilingual_v2,
    lipsync-2.0) which use a parallel non-BUILDERS adapter shape — they
    are excluded from this check.

    CP-9 (Phase 3) adds the text_multimodal eval modality
    (gemini-3.1-pro-preview → gemini_vision) which is reached via the
    Phase 2 score_artifact() function rather than a BUILDERS prompt
    builder; it is excluded for the same reason.
    """
    import json
    strategy_path = _RECOIL_ROOT / "config" / "provider_strategy.json"
    with open(strategy_path) as f:
        strategy = json.load(f)
    profiles_path = _RECOIL_ROOT / "config" / "model_profiles.json"
    with open(profiles_path) as f:
        profiles = json.load(f)
    NON_BUILDER_MODALITIES = {"audio_t2a", "lipsync_post", "text_multimodal"}
    strategy_models = {
        k for k in strategy
        if not k.startswith('__')
        and k != "schema_version"
        and isinstance(profiles.get(k), dict)
        and profiles.get(k, {}).get("modality") not in NON_BUILDER_MODALITIES
    }
    builders_models = {mid for (mid, _) in BUILDERS}
    missing = strategy_models - builders_models
    assert not missing, (
        f"BUILDERS missing model_ids from provider_strategy.json: {missing}"
    )
