import json

import pytest

from player.ring import RingMove, ScaleRing, advance_ring, resolve_move
from simulator.clips import (
    Clip,
    load_manifest,
    load_ring,
    resolved_move_to_dict,
    ring_to_dict,
)


def _manifest_dict():
    return {
        "clips": [
            {
                "id": "forest",
                "title": "Yosemite Falls (neutral)",
                "base_file": "forest/base.mp4",
                "license": "poc-sample",
                "source": "hef-poc",
                "right_variants": {
                    "4": {"file": "forest/right4.mp4", "model": "sd-turbo+flow"},
                    "1": {"file": "forest/right1.mp4"},
                },
                "annotations": [
                    {"key": "detected.water", "box": [0.1, 0.2, 0.3, 0.4], "min_level": 1},
                    {"key": "detected.conifer", "box": [0.6, 0.1, 0.2, 0.2], "min_level": 3},
                ],
                "strings": {"en": {"detected.water": "flowing water", "detected.conifer": "conifer"}},
            }
        ]
    }


def test_load_manifest_parses_clips(tmp_path):
    p = tmp_path / "manifest.json"
    p.write_text(json.dumps(_manifest_dict()))
    clips = load_manifest(p)
    assert len(clips) == 1
    c = clips[0]
    assert isinstance(c, Clip)
    assert c.id == "forest"
    assert c.base_file == "forest/base.mp4"


def test_clip_lists_variant_files_by_strength(tmp_path):
    p = tmp_path / "manifest.json"
    p.write_text(json.dumps(_manifest_dict()))
    c = load_manifest(p)[0]
    # variant 0 is always the raw base; authored strengths come from the manifest
    assert c.variant_file(0) == "forest/base.mp4"
    assert c.variant_file(4) == "forest/right4.mp4"
    assert c.variant_file(1) == "forest/right1.mp4"
    # an unauthored strength falls back to the raw base
    assert c.variant_file(2) == "forest/base.mp4"


def test_clip_serializes_to_dict_for_the_api(tmp_path):
    p = tmp_path / "manifest.json"
    p.write_text(json.dumps(_manifest_dict()))
    d = load_manifest(p)[0].to_dict()
    assert d["id"] == "forest"
    assert d["base_file"] == "forest/base.mp4"
    assert d["annotations"][0]["key"] == "detected.water"
    assert d["strings"]["en"]["detected.water"] == "flowing water"
    # variant map is exposed keyed by strength string, including 0 -> base
    assert d["right_variants"]["0"]["file"] == "forest/base.mp4"
    assert d["right_variants"]["4"]["file"] == "forest/right4.mp4"


def test_missing_manifest_raises(tmp_path):
    with pytest.raises(FileNotFoundError):
        load_manifest(tmp_path / "nope.json")


def _ring_manifest_dict():
    return {
        "clips": [],
        "ring": {
            "scales": [
                {"id": "cosmos", "clip_id": "cosmos"},
                {"id": "forest", "clip_id": "forest"},
                {"id": "abyss", "clip_id": "abyss"},
            ],
            "transitions": [
                {"from": "cosmos", "to": "forest", "file": "transitions/cosmos__forest.mp4", "model": "placeholder"},
                {"from": "forest", "to": "cosmos", "file": "transitions/cosmos__forest.rev.mp4", "model": "placeholder"},
                {"from": "forest", "to": "abyss", "file": "transitions/forest__abyss.mp4", "model": "placeholder"},
                {"from": "abyss", "to": "forest", "file": "transitions/forest__abyss.rev.mp4", "model": "placeholder"},
                {"from": "abyss", "to": "cosmos", "file": "transitions/abyss__cosmos.mp4", "model": "placeholder"},
                {"from": "cosmos", "to": "abyss", "file": "transitions/abyss__cosmos.rev.mp4", "model": "placeholder"},
            ],
        },
    }


def test_load_ring_builds_a_scale_ring(tmp_path):
    p = tmp_path / "manifest.json"
    p.write_text(json.dumps(_ring_manifest_dict()))
    ring = load_ring(p)
    assert isinstance(ring, ScaleRing)
    assert len(ring) == 3
    assert ring.scales[0].id == "cosmos"
    assert ring.scales[0].clip_id == "cosmos"
    assert ring.morph_for("abyss", "cosmos") == "transitions/abyss__cosmos.mp4"   # zoom in (wrap)
    assert ring.morph_for("cosmos", "forest") == "transitions/cosmos__forest.mp4"
    assert ring.transitions[0].model == "placeholder"


def test_load_ring_is_none_when_absent(tmp_path):
    p = tmp_path / "manifest.json"
    p.write_text(json.dumps(_manifest_dict()))  # no "ring" key
    assert load_ring(p) is None


def test_ring_to_dict_carries_scale_titles_from_clips(tmp_path):
    p = tmp_path / "manifest.json"
    md = _ring_manifest_dict()
    md["clips"] = [
        {"id": "cosmos", "title": "NASA cosmos", "base_file": "cosmos/base.mp4"},
        {"id": "forest", "title": "Yosemite", "base_file": "forest/base.mp4"},
        {"id": "abyss", "title": "NOAA abyss", "base_file": "abyss/base.mp4"},
    ]
    p.write_text(json.dumps(md))
    ring = load_ring(p)
    clips = load_manifest(p)
    d = ring_to_dict(ring, clips)
    assert [s["id"] for s in d["scales"]] == ["cosmos", "forest", "abyss"]
    assert d["scales"][0]["title"] == "NASA cosmos"
    assert d["scales"][0]["clip_id"] == "cosmos"
    assert d["transitions"][0] == {
        "from": "cosmos", "to": "forest",
        "file": "transitions/cosmos__forest.mp4", "model": "placeholder",
    }


def test_resolved_move_to_dict_serializes_chained_morphs(tmp_path):
    p = tmp_path / "manifest.json"
    p.write_text(json.dumps(_ring_manifest_dict()))
    ring = load_ring(p)
    move = advance_ring(ring, from_index=2, delta=1)  # abyss -> wrap -> cosmos
    resolved = resolve_move(ring, move, from_clip_id="abyss", picks=(0.0,))
    d = resolved_move_to_dict(move, resolved)
    assert d["from_index"] == 2
    assert d["to_index"] == 0
    assert d["wrapped"] is True
    assert d["target_clip_id"] == "cosmos"
    assert d["steps"][0]["from_clip"] == "abyss"
    assert d["steps"][0]["to_clip"] == "cosmos"
    assert d["steps"][0]["file"] == "transitions/abyss__cosmos.mp4"   # zoom in (wrap)
    assert d["steps"][0]["to_index"] == 0
    assert d["fast"] is False
    assert d["steps"][0]["blended"] is False


def test_resolved_move_to_dict_marks_fast_blended_chain(tmp_path):
    p = tmp_path / "manifest.json"
    p.write_text(json.dumps(_ring_manifest_dict()))
    ring = load_ring(p)
    move = advance_ring(ring, from_index=0, delta=4, fast_spin_threshold=3)
    resolved = resolve_move(ring, move, from_clip_id="cosmos",
                            picks=tuple(0.0 for _ in move.steps))
    d = resolved_move_to_dict(move, resolved)
    assert d["fast"] is True
    assert len(d["steps"]) == 4                       # whole chain kept, played fast
    assert all(s["blended"] for s in d["steps"])
