"""GET /video/studio/presets + POST /video/studio/presets/<id>/apply — the studio
preset HTTP surface + the routability of every seeded preset.

Written in the STUDIO script style (plain python, ``__main__`` guard, numbered
``[n] PASS`` / ``[n] FAIL`` lines, every check independently run so a failing one
never masks the rest, nonzero exit iff any check FAILED) — pytest is NOT installed
in this venv (see tests/studio/test_studio_t2v.py).

It locks three things:
  * WIRE SHAPE — GET /video/studio/presets returns {"presets":[...]} carrying (at
    least) the known seed presets, each in the pinned per-preset shape; the apply
    envelope wraps a directly-POSTable /video/studio/i2v body; an unknown id -> 404.
  * STRUCTURAL VALIDITY — every seeded preset's ``request_body()`` is accepted by
    ``studio.job.make_studio_i2v`` verbatim (make_studio_i2v(**request_body())).
  * ROUTABILITY + BINDING INTENT — every preset RESOLVES through the studio router
    (studio.router.CapabilityRouter) to a model, and binds the INTENDED class
    (synthetic prover for the tiny-budget previews; a real Wan model for the rest).

Run:
  cd /srv/share/projects/hugpy/dev/abstract_hugpy_dev
  venv/bin/python tests/test_studio_presets_route.py
"""
from __future__ import annotations

import atexit
import logging
import os
import shutil
import sqlite3
import subprocess
import sys
import tempfile
import time

logging.disable(logging.INFO)  # silence the models_config registry chatter

# Unpinned dev tree — the studio zoo is all unpinned in this slice; the router
# resolve() does not gate on it, but keep parity with the studio suites.
os.environ.setdefault("STUDIO_ALLOW_UNPINNED", "1")
# Keep any bus/audit writes out of the real projects tree (mirrors the video
# preset route test).
os.environ.setdefault(
    "PROJECTS_HOME", tempfile.mkdtemp(prefix="hugpy-studio-presets-test-"))

_SRC = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src")
if _SRC not in sys.path:
    sys.path.insert(0, _SRC)

import importlib  # noqa: E402

from flask import Flask  # noqa: E402

from abstract_hugpy_dev.video_intel.studio_presets import (  # noqa: E402
    available_studio_presets,
    get_studio_preset,
)
from abstract_hugpy_dev.video_intel.studio.job import make_studio_i2v, StudioI2VSpec  # noqa: E402
from abstract_hugpy_dev.video_intel.studio.router import CapabilityRouter  # noqa: E402
from abstract_hugpy_dev.video_intel.studio.schemas import CapabilityRequest, Resolution  # noqa: E402
from abstract_hugpy_dev.video_intel.studio.enums import Capability, Framework  # noqa: E402
from abstract_hugpy_dev.video_intel import media_bus  # noqa: E402
from abstract_hugpy_dev.imports.src.constants.constants import DEFAULT_ROOT  # noqa: E402

# Isolation (only the v2v enqueue check below actually enqueues): point the media
# bus at a TEMP DB so POST /video/studio/i2v never writes a row into the REAL dev
# catalog (DEFAULT_ROOT/video_intel/media_jobs.db). Mirrors test_studio_source_video.
_TMP_DB = tempfile.mkstemp(prefix="studio-presets-bus-", suffix=".db")[1]
media_bus.DB_PATH = _TMP_DB
media_bus._initialized = False  # force _ensure_db to re-init against the temp DB
with sqlite3.connect(_TMP_DB) as _c:
    _c.execute(
        "CREATE TABLE IF NOT EXISTS media_jobs ("
        "job_id TEXT PRIMARY KEY, name TEXT, status TEXT, spec_json TEXT, "
        "result_json TEXT, claim_token TEXT, created REAL, updated REAL, "
        "progress_json TEXT)")


@atexit.register
def _cleanup_tmp_db() -> None:
    try:
        os.remove(_TMP_DB)
    except OSError:
        pass


_FFMPEG = shutil.which("ffmpeg") is not None
_FFPROBE = shutil.which("ffprobe") is not None


def _make_tiny_mp4(path: str) -> None:
    """A real 2s 160x120 mp4 (ffprobe classifies it as a video), so the route's
    source_video validation (jail -> media_store.ingest ffprobe classify) accepts it."""
    ffmpeg = shutil.which("ffmpeg") or "ffmpeg"
    subprocess.run(
        [ffmpeg, "-y", "-f", "lavfi", "-i",
         "testsrc=duration=2:size=160x120:rate=8", "-pix_fmt", "yuv420p", path],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)


vr = importlib.import_module(
    "abstract_hugpy_dev.flask_app.app.routes.video_routes")

app = Flask(__name__)
app.register_blueprint(vr.video_bp)
client = app.test_client()

# The pinned per-preset wire shape (subset-checked so future additions don't break).
_TOP_KEYS = {"id", "name", "description", "capability", "width", "height", "fps",
             "vram_budget_gb", "seed", "prompt", "negative", "recommended"}
_REQUEST_KEYS = {"capability", "width", "height", "fps", "vram_budget_gb", "seed",
                 "prompt", "negative"}

# The known preset ids -> intended router binding. Subset-checked, so future
# additions won't break this test. "synthetic" means the tiny-budget prover binds
# (framework == SYNTHETIC); a model_id means a real model must bind exactly.
#   ("<framework>", "<model_id or None>")
_EXPECTED = {
    "quick-preview-synthetic": (Framework.SYNTHETIC, "synthetic-i2v"),
    "preview-t2v-synthetic":   (Framework.SYNTHETIC, "synthetic-t2v"),
    "cinematic-720p-i2v":      (Framework.WAN, "wan2.1-i2v-14b-720p"),
    "portrait-720p-i2v":       (Framework.WAN, "wan2.1-i2v-14b-720p"),
    "wan-t2v-1.3b-3090":       (Framework.WAN, "wan2.1-t2v-1.3b"),
    "max-quality-t2v":         (Framework.WAN, "wan2.2-t2v-a14b"),
    # Two first-class TIERS (this slice): the DRAFT (1.3B t2v @ 9 GB) and QUALITY
    # (14B i2v @ 16 GB) one-click presets. Router-verified like the other seeds.
    "draft-t2v-1.3b":          (Framework.WAN, "wan2.1-t2v-1.3b"),
    "quality-i2v-14b-int8":    (Framework.WAN, "wan2.1-i2v-14b-720p"),
    # Slice (a) / v2v RESTYLE: capability v2v @ 832x480 @ 6 GB is the only V2V bind
    # that fits (the 14B needs 14 GB+), so it resolves to the Wan 2.1 VACE 1.3B model.
    "restyle-480p-v2v":        (Framework.WAN, "wan2.1-vace-1.3b"),
}


def _resolve(preset):
    """Resolve a preset EXACTLY as the bus runner does (runners/studio_i2v.py):
    lift capability + geometry + budget into a CapabilityRequest and resolve."""
    req = CapabilityRequest(
        capability=Capability(preset.capability),
        target_resolution=Resolution(preset.width, preset.height, preset.fps),
        vram_budget_gb=preset.vram_budget_gb,
    )
    return CapabilityRouter().resolve(req)


# --------------------------------------------------------------------------- #
# [1] GET /video/studio/presets — {presets:[...]} envelope, pinned shape present.
# --------------------------------------------------------------------------- #
def test_get_studio_presets_contract_shape():
    r = client.get("/video/studio/presets")
    assert r.status_code == 200, r.status_code
    body = r.get_json()
    assert isinstance(body, dict) and "presets" in body, body
    presets = body["presets"]
    assert isinstance(presets, list) and presets, presets

    by_id = {p["id"]: p for p in presets}
    assert set(_EXPECTED) <= set(by_id), set(by_id)  # subset — additions are fine
    for pid in _EXPECTED:
        p = by_id[pid]
        assert _TOP_KEYS <= set(p), (pid, set(p))
        assert p["capability"] in ("i2v", "t2v", "v2v"), (pid, p["capability"])
        assert isinstance(p["width"], int) and p["width"] > 0, (pid, p["width"])
        assert isinstance(p["height"], int) and p["height"] > 0, (pid, p["height"])
        assert isinstance(p["vram_budget_gb"], (int, float)), (pid, p["vram_budget_gb"])


# --------------------------------------------------------------------------- #
# [2] POST /video/studio/presets/<id>/apply — pure-prefill envelope wrapping a
#     directly-POSTable /video/studio/i2v request body (no worker side-effects).
# --------------------------------------------------------------------------- #
def test_apply_envelope_shape():
    for pid in _EXPECTED:
        r = client.post(f"/video/studio/presets/{pid}/apply")
        assert r.status_code == 200, (pid, r.status_code)
        body = r.get_json()
        assert body.get("ok") is True, (pid, body)
        assert body.get("id") == pid, (pid, body)
        assert "capability" in body, (pid, body)
        req = body.get("request")
        assert isinstance(req, dict), (pid, req)
        assert _REQUEST_KEYS <= set(req), (pid, set(req))
        # the request echoes the preset's capability + geometry
        preset = get_studio_preset(pid)
        assert req["capability"] == preset.capability, (pid, req["capability"])
        assert req["width"] == preset.width and req["height"] == preset.height, (pid, req)


# --------------------------------------------------------------------------- #
# [3] Unknown id -> 404 (get_studio_preset -> None short-circuits before any work).
# --------------------------------------------------------------------------- #
def test_apply_unknown_preset_404():
    r = client.post("/video/studio/presets/does-not-exist/apply")
    assert r.status_code == 404, r.status_code
    body = r.get_json()
    assert body.get("ok") is False, body
    assert body.get("error", {}).get("code") == "UnknownPreset", body


# --------------------------------------------------------------------------- #
# [4] STRUCTURAL VALIDITY — every seeded preset's request_body() is accepted by
#     make_studio_i2v verbatim (make_studio_i2v(**request_body()) -> StudioI2VSpec).
# --------------------------------------------------------------------------- #
def test_request_body_accepted_by_make_studio_i2v():
    for preset in available_studio_presets():
        spec = make_studio_i2v(**preset.request_body())
        assert isinstance(spec, StudioI2VSpec), (preset.id, type(spec))
        assert spec.capability == preset.capability, (preset.id, spec.capability)
        assert spec.width == preset.width and spec.height == preset.height, preset.id
        assert spec.fps == preset.fps, (preset.id, spec.fps)
        assert spec.vram_budget_gb == float(preset.vram_budget_gb), preset.id
        assert spec.seed == preset.seed, (preset.id, spec.seed)


# --------------------------------------------------------------------------- #
# [5] ROUTABILITY — every preset RESOLVES through the studio router to a model.
# --------------------------------------------------------------------------- #
def test_every_preset_routes_to_a_model():
    for preset in available_studio_presets():
        res = _resolve(preset)
        assert res.is_ok(), (
            f"{preset.id}: must route to a model; got {res.error.code if res.is_err() else res}")
        binding = res.unwrap()
        assert binding.model_id, (preset.id, binding)


# --------------------------------------------------------------------------- #
# [6] BINDING INTENT — the tiny-budget previews bind the SYNTHETIC prover; the
#     real-budget presets bind their intended REAL Wan model (never synthetic).
# --------------------------------------------------------------------------- #
def test_preset_binding_intent():
    for pid, (exp_fw, exp_model) in _EXPECTED.items():
        preset = get_studio_preset(pid)
        assert preset is not None, pid
        binding = _resolve(preset).unwrap()
        assert binding.framework == exp_fw, (
            f"{pid}: expected framework {exp_fw.value}; got {binding.framework.value} "
            f"(model {binding.model_id})")
        assert binding.model_id == exp_model, (
            f"{pid}: expected model {exp_model}; got {binding.model_id}")
        if exp_fw is Framework.SYNTHETIC:
            assert binding.framework == Framework.SYNTHETIC, pid
        else:
            # a real preset must NEVER fall back to the synthetic prover
            assert binding.framework != Framework.SYNTHETIC, (
                f"{pid}: a real preset must not bind the synthetic prover")


# --------------------------------------------------------------------------- #
# [7] V2V RESTYLE signal — the restyle-480p-v2v preset carries requires_source on
#     BOTH the GET list shape and the POST /apply envelope (the UI's dead-on-arrival
#     guard reads it), and bakes the VALID VACE envelope (v2v @ 832x480, LANDSCAPE).
#     request_body() stays source-free (source is threaded from the staged clip, not
#     the preset) — proven POSTable in [8].
# --------------------------------------------------------------------------- #
def test_v2v_restyle_requires_source_signal():
    preset = get_studio_preset("restyle-480p-v2v")
    assert preset is not None, "restyle-480p-v2v must be registered"
    assert preset.capability == "v2v", preset.capability
    assert preset.requires_source is True, "the restyle preset must require a source"
    # VALID VACE geometry baked in (landscape, within the 832x480 envelope) so the
    # preset can never dead-on-arrive: 832 >= width AND 480 >= height (portrait rejects).
    assert (preset.width, preset.height) == (832, 480), (preset.width, preset.height)
    assert 832 >= preset.width and 480 >= preset.height, (preset.width, preset.height)

    # GET list carries the signal in the pinned per-preset shape.
    body = client.get("/video/studio/presets").get_json()
    row = next(p for p in body["presets"] if p["id"] == "restyle-480p-v2v")
    assert row.get("requires_source") is True, row
    assert row["capability"] == "v2v", row

    # POST /apply surfaces requires_source at the envelope TOP LEVEL (the UI gate),
    # and its request echoes the v2v capability + baked geometry.
    ap = client.post("/video/studio/presets/restyle-480p-v2v/apply").get_json()
    assert ap.get("ok") is True, ap
    assert ap.get("requires_source") is True, ap
    assert ap.get("capability") == "v2v", ap
    assert ap["request"]["capability"] == "v2v", ap["request"]
    assert ap["request"]["width"] == 832 and ap["request"]["height"] == 480, ap["request"]
    # requires_source is a UI signal, NOT a make_studio_i2v keyword — it must NOT leak
    # into the POSTable request body (which is validated verbatim in [4]/[8]).
    assert "requires_source" not in ap["request"], ap["request"]


# --------------------------------------------------------------------------- #
# [8] V2V RESTYLE end-to-end route: the preset's request_body() + a staged source
#     clip POSTs 200 {job_id} through /video/studio/i2v (the real restyle enqueue:
#     v2v capability routes to VACE-1.3B, the source is jail-resolved + ffprobe-
#     classified). request_body() alone carries NO source (the source is the staged
#     clip), so this is exactly what the Studio Clips station sends for a restyle.
# --------------------------------------------------------------------------- #
def test_v2v_restyle_request_body_with_source_posts_200():
    if not (_FFMPEG and _FFPROBE):
        print("      (ffmpeg/ffprobe unavailable — skipping v2v route enqueue check)")
        return
    preset = get_studio_preset("restyle-480p-v2v")
    work = tempfile.mkdtemp(prefix="studio-presets-v2v-", dir=DEFAULT_ROOT)  # inside jail
    src = os.path.join(work, "restyle_source.mp4")
    try:
        _make_tiny_mp4(src)
        # request_body() is the source-free curated body; the station adds the staged
        # source clip at enqueue time — replicate that here.
        body = dict(preset.request_body())
        body["source_video"] = src
        r = client.post("/video/studio/i2v", json=body)
        assert r.status_code == 200, (r.status_code, r.get_json())
        assert isinstance(r.get_json().get("job_id"), str), r.get_json()
    finally:
        shutil.rmtree(work, ignore_errors=True)


CHECKS = [
    ("GET /video/studio/presets: {presets:[...]} envelope, pinned per-preset shape",
     test_get_studio_presets_contract_shape),
    ("POST .../apply: pure-prefill envelope wrapping a POSTable /video/studio/i2v body",
     test_apply_envelope_shape),
    ("POST .../<unknown>/apply -> 404 UnknownPreset",
     test_apply_unknown_preset_404),
    ("every preset's request_body() is accepted by make_studio_i2v (structural)",
     test_request_body_accepted_by_make_studio_i2v),
    ("every preset RESOLVES through the studio router to a model",
     test_every_preset_routes_to_a_model),
    ("binding intent: synthetic prover for previews, real Wan model for the rest",
     test_preset_binding_intent),
    ("v2v restyle: requires_source signal on the GET list + POST /apply envelope",
     test_v2v_restyle_requires_source_signal),
    ("v2v restyle: request_body() + staged source clip POSTs 200 {job_id}",
     test_v2v_restyle_request_body_with_source_posts_200),
]


def main() -> int:
    passed = 0
    failed = 0
    for i, (name, fn) in enumerate(CHECKS, 1):
        try:
            fn()
        except Exception as exc:  # surface EVERY divergence, not just the first
            failed += 1
            print(f"[{i}] FAIL  {name}: {type(exc).__name__}: {exc}")
        else:
            passed += 1
            print(f"[{i}] PASS  {name}")
    print(f"\n{passed} passed, {failed} failed of {len(CHECKS)}")
    return 1 if failed else 0


if __name__ == "__main__":
    raise SystemExit(main())
