"""video_generate dynamic schema — capability-gated params (#95681 diet).

Mirrors tests/tools/test_image_generate_schema.py (#97057). Coverage is
guaranteed two ways:
1. every in-tree video_gen plugin's capabilities() must declare EVERY axis
   the schema builder reads (a new axis added to the builder without fleet
   declarations fails here);
2. every FAL video family must carry the per-family keys the fal provider's
   active-model capabilities() resolution reads;
"""
import os
import sys
import unittest
from unittest.mock import patch

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))

import tools.video_generation_tool as vt
from tools.video_generation_tool import (
    _build_dynamic_video_schema,
)

# Every axis _build_dynamic_video_schema reads from capabilities().
CAPABILITY_AXES = (
    "modalities",
    "aspect_ratios",
    "resolutions",
    "max_duration",
    "min_duration",
    "supports_audio",
    "supports_negative_prompt",
    "supports_seed",
    "supports_upscale",
    "max_reference_images",
)

# Per-family keys the FAL provider's capabilities() resolution reads.
FAL_FAMILY_KEYS = ("durations", "aspect_ratios", "resolutions", "audio",
                   "negative", "seed")


class TestFleetCapabilityCoverage(unittest.TestCase):
    def test_every_provider_declares_every_axis(self):
        """Instantiate each in-tree provider class and check the RETURNED
        capabilities dict — source grep can't see inherited keys."""
        checked = 0
        # fal
        from plugins.video_gen.fal import FALVideoGenProvider

        caps = FALVideoGenProvider().capabilities()
        for axis in CAPABILITY_AXES:
            self.assertIn(axis, caps, f"fal missing {axis}")
        checked += 1
        # xai
        from plugins.video_gen.xai import XAIVideoGenProvider

        caps = XAIVideoGenProvider().capabilities()
        for axis in CAPABILITY_AXES:
            self.assertIn(axis, caps, f"xai missing {axis}")
        checked += 1
        # deepinfra
        from plugins.video_gen.deepinfra import DeepInfraVideoGenProvider

        caps = DeepInfraVideoGenProvider().capabilities()
        for axis in CAPABILITY_AXES:
            self.assertIn(axis, caps, f"deepinfra missing {axis}")
        checked += 1
        # openrouter
        from plugins.video_gen.openrouter import OpenRouterVideoGenProvider

        caps = OpenRouterVideoGenProvider().capabilities()
        for axis in CAPABILITY_AXES:
            self.assertIn(axis, caps, f"openrouter missing {axis}")
        checked += 1
        self.assertGreaterEqual(checked, 4)

    def test_abc_default_fails_closed(self):
        from agent.video_gen_provider import VideoGenProvider

        class _P(VideoGenProvider):
            name = "t"
            display_name = "T"
            def generate(self, prompt, **kw):
                return {}
            def list_models(self):
                return []
        caps = _P().capabilities()
        self.assertEqual(caps.get("modalities"), ["text"])
        for axis in ("supports_audio", "supports_negative_prompt",
                     "supports_seed", "supports_upscale"):
            self.assertFalse(caps.get(axis), axis)
        for axis in CAPABILITY_AXES:
            self.assertIn(axis, caps, f"ABC default missing {axis}")

    def test_every_fal_family_declares_per_family_keys(self):
        from plugins.video_gen.fal import FAL_FAMILIES

        for fam, meta in FAL_FAMILIES.items():
            with self.subTest(family=fam):
                for key in FAL_FAMILY_KEYS:
                    self.assertIn(key, meta, f"{fam} missing {key}")
                self.assertTrue(
                    meta.get("text_endpoint") or meta.get("image_endpoint"),
                    f"{fam} declares no endpoint",
                )
                # Audio truth model: "audio" means a generate_audio TOGGLE
                # exists; "audio_native" means audio is ALWAYS ON with no
                # toggle. A family may have neither (silent model) but
                # never both — that would be contradictory.
                self.assertFalse(
                    meta.get("audio") and meta.get("audio_native"),
                    f"{fam}: audio toggle and always-on are mutually "
                    "exclusive",
                )

    def test_always_on_audio_surfaces_in_description_not_param(self):
        """Maintainer catch (H3 has audio!): families whose audio is
        always-on (no API toggle) must TELL the model about the audio in
        the description rather than advertise a dead `audio` param."""
        schema = TestDynamicParamGating._schema_with(
            TestDynamicParamGating(), {
                "modalities": ["text", "image"],
                "supports_audio": False, "audio_always_on": True,
                "supports_negative_prompt": False, "supports_seed": False,
                "supports_upscale": False, "max_reference_images": 0,
            })
        self.assertNotIn("audio", schema["parameters"]["properties"])
        self.assertIn("always on", schema["description"])



class TestDynamicParamGating(unittest.TestCase):
    def _schema_with(self, caps, model_meta=None):
        class _Prov:
            name = "fake"
            display_name = "Fake"
            def capabilities(self):
                return caps
            def list_models(self):
                return [dict({"id": "m1"}, **(model_meta or {}))]
            def default_model(self):
                return "m1"
        with patch.object(vt, "_resolve_active_provider",
                          return_value=_Prov()), \
             patch.object(vt, "_read_configured_video_model",
                          return_value="m1"):
            return _build_dynamic_video_schema()

    def test_full_featured_backend_gets_all_params(self):
        schema = self._schema_with({
            "modalities": ["text", "image"],
            "aspect_ratios": ["16:9"], "resolutions": ["720p"],
            "min_duration": 2, "max_duration": 12,
            "supports_audio": True, "supports_negative_prompt": True,
            "supports_seed": True, "supports_upscale": True,
            "max_reference_images": 7,
        })
        props = schema["parameters"]["properties"]
        for p in ("image_url", "reference_image_urls", "negative_prompt",
                  "audio", "seed", "upscale"):
            self.assertIn(p, props, p)
        self.assertEqual(props["reference_image_urls"]["maxItems"], 7)
        self.assertEqual(props["duration"]["minimum"], 2)
        self.assertEqual(props["duration"]["maximum"], 12)
        self.assertEqual(props["aspect_ratio"]["enum"], ["16:9"])

    def test_minimal_backend_gets_bare_params(self):
        schema = self._schema_with({
            "modalities": ["text"],
            "supports_audio": False, "supports_negative_prompt": False,
            "supports_seed": False, "supports_upscale": False,
            "max_reference_images": 0,
        })
        props = schema["parameters"]["properties"]
        for p in ("image_url", "reference_image_urls", "negative_prompt",
                  "audio", "seed", "upscale"):
            self.assertNotIn(p, props, p)
        self.assertIn("text-to-video only", schema["description"])

    def test_i2v_only_model_overrides_backend_union(self):
        # gemini-omni-flash case: dual-modality backend, i2v-only model.
        schema = self._schema_with(
            {"modalities": ["text", "image"], "max_reference_images": 0,
             "supports_audio": False, "supports_negative_prompt": False,
             "supports_seed": False, "supports_upscale": False},
            model_meta={"modalities": ["image"]},
        )
        self.assertIn("image_url", schema["parameters"]["properties"])
        self.assertIn("image-to-video only", schema["description"])

    def test_no_provider_serves_prompt_only(self):
        with patch.object(vt, "_resolve_active_provider", return_value=None):
            schema = _build_dynamic_video_schema()
        self.assertEqual(sorted(schema["parameters"]["properties"]), ["prompt"])



if __name__ == "__main__":
    unittest.main()
