#!/usr/bin/env python3
"""Live tests for Resolve 20.x scripting additions.

Creates a temporary Resolve project, imports synthetic media generated by the
test, exercises additive Resolve 20 APIs, then restores the original project.
"""

import base64
import json
import math
import os
import shutil
import struct
import sys
import time
import wave

sys.path.insert(
    0,
    "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules",
)

import DaVinciResolveScript as dvr  # noqa: E402


PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TMP_ROOT = os.path.join(PROJECT_ROOT, "tmp", "resolve20-live")
TEST_PROJECT = f"_mcp_resolve20_api_{int(time.time())}"

PNG_1X1 = (
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR4nGNgYGD4DwABBAEA"
    "ghO+9wAAAABJRU5ErkJggg=="
)


results = {"pass": [], "fail": [], "skip": []}


def print_connection_diagnostics():
    print("ERROR: Cannot connect to DaVinci Resolve")
    print(f"Python: {sys.executable} ({sys.version.split()[0]})")
    print(f"DaVinciResolveScript: {getattr(dvr, '__file__', '<unknown>')}")
    print(f"RESOLVE_SCRIPT_API={os.environ.get('RESOLVE_SCRIPT_API', '<unset>')}")
    print(f"RESOLVE_SCRIPT_LIB={os.environ.get('RESOLVE_SCRIPT_LIB', '<unset>')}")
    print(
        "Check Resolve Studio Preferences > System > General > "
        "External scripting using, set it to Local, then restart Resolve."
    )


def record(kind, name, value=""):
    results[kind].append((name, str(value)[:160]))
    print(f"  {kind.upper():5} {name}: {str(value)[:120]}")


def run_live_check(name, fn=None, skip_reason=None, allow_false=True):
    if skip_reason or fn is None:
        record("skip", name, skip_reason or "no test function")
        return None
    try:
        value = fn()
        if value is False and not allow_false:
            record("fail", name, "returned False")
        else:
            record("pass", name, value)
        return value
    except Exception as exc:
        record("fail", name, f"{type(exc).__name__}: {exc}")
        return None


def write_wav(path, freq):
    sample_rate = 48000
    duration = 0.5
    frames = int(sample_rate * duration)
    with wave.open(path, "wb") as wav:
        wav.setnchannels(1)
        wav.setsampwidth(2)
        wav.setframerate(sample_rate)
        for i in range(frames):
            value = int(12000 * math.sin(2 * math.pi * freq * i / sample_rate))
            wav.writeframes(struct.pack("<h", value))


def write_media_files(base_dir):
    os.makedirs(base_dir, exist_ok=True)
    image_path = os.path.join(base_dir, "resolve20_still.png")
    wav_a = os.path.join(base_dir, "resolve20_a.wav")
    wav_b = os.path.join(base_dir, "resolve20_b.wav")
    with open(image_path, "wb") as handle:
        handle.write(base64.b64decode(PNG_1X1))
    write_wav(wav_a, 440)
    write_wav(wav_b, 880)
    return image_path, wav_a, wav_b


def find_clip_by_path(clips, path):
    want = os.path.abspath(path)
    for clip in clips or []:
        try:
            clip_path = clip.GetClipProperty("File Path")
        except Exception:
            clip_path = ""
        if clip_path and os.path.abspath(clip_path) == want:
            return clip
    return None


def first_item(timeline, track_type):
    count = timeline.GetTrackCount(track_type) or 0
    for track_index in range(1, count + 1):
        items = timeline.GetItemListInTrack(track_type, track_index) or []
        if items:
            return track_index, items[0]
    return None, None


def cleanup(pm, original_project_name):
    try:
        if original_project_name:
            pm.LoadProject(original_project_name)
    finally:
        try:
            pm.DeleteProject(TEST_PROJECT)
        except Exception:
            pass
        shutil.rmtree(TMP_ROOT, ignore_errors=True)


def main():
    resolve = dvr.scriptapp("Resolve")
    if not resolve:
        print_connection_diagnostics()
        return 1

    print(f"Connected to {resolve.GetProductName()} {resolve.GetVersionString()}")
    version = resolve.GetVersion() or [0]
    if version[0] < 20:
        print("ERROR: Resolve 20+ is required for this test")
        return 1

    pm = resolve.GetProjectManager()
    original_project = pm.GetCurrentProject()
    original_project_name = original_project.GetName() if original_project else None
    if original_project_name == "Untitled Project" and os.environ.get("RESOLVE20_LIVE_ALLOW_UNTITLED") != "1":
        print(
            "ERROR: Current project is 'Untitled Project'. Save or close it first, "
            "or set RESOLVE20_LIVE_ALLOW_UNTITLED=1 if this is a disposable empty project."
        )
        return 1

    shutil.rmtree(TMP_ROOT, ignore_errors=True)
    media_dir = os.path.join(TMP_ROOT, "media")
    media_location = os.path.join(TMP_ROOT, "project-media")
    os.makedirs(media_location, exist_ok=True)
    image_path, wav_a, wav_b = write_media_files(media_dir)

    project = None
    try:
        project = run_live_check(
            "PM.CreateProject(mediaLocationPath)",
            lambda: pm.CreateProject(TEST_PROJECT, media_location),
            allow_false=False,
        )
        if not project:
            return 1

        mp = project.GetMediaPool()
        clips = run_live_check(
            "MediaPool.ImportMedia(resolve20 fixtures)",
            lambda: mp.ImportMedia([image_path, wav_a, wav_b]),
            allow_false=False,
        )
        if not clips:
            return 1

        image_clip = find_clip_by_path(clips, image_path) or clips[0]
        audio_clip = find_clip_by_path(clips, wav_a)
        replacement_audio = wav_b

        timeline = run_live_check(
            "MP.CreateTimelineFromClips(resolve20 fixtures)",
            lambda: mp.CreateTimelineFromClips("_resolve20_api_timeline", clips),
            allow_false=False,
        )
        if not timeline:
            return 1
        project.SetCurrentTimeline(timeline)

        audio_track, audio_item = first_item(timeline, "audio")
        video_track, video_item = first_item(timeline, "video")

        run_live_check("Resolve.GetFairlightPresets", lambda: resolve.GetFairlightPresets())
        presets = resolve.GetFairlightPresets() or []
        preset_name = presets[0] if presets else "__missing_resolve20_test_preset__"
        run_live_check(
            "Project.ApplyFairlightPresetToCurrentTimeline",
            lambda: project.ApplyFairlightPresetToCurrentTimeline(preset_name),
        )

        run_live_check(
            "Project.SetRenderSettings subtitle keys",
            lambda: project.SetRenderSettings(
                {"ExportSubtitle": False, "SubtitleFormat": "BurnIn"}
            ),
        )

        if audio_clip:
            original_name = audio_clip.GetName()
            run_live_check("MPI.SetName", lambda: audio_clip.SetName(f"{original_name}_renamed"))
            audio_clip.SetName(original_name)
            run_live_check(
                "MPI.LinkFullResolutionMedia",
                lambda: audio_clip.LinkFullResolutionMedia(replacement_audio),
            )
            run_live_check(
                "MPI.ReplaceClipPreserveSubClip",
                lambda: audio_clip.ReplaceClipPreserveSubClip(replacement_audio),
            )
            run_live_check("MPI.MonitorGrowingFile", lambda: audio_clip.MonitorGrowingFile())
        else:
            for name in (
                "MPI.SetName",
                "MPI.LinkFullResolutionMedia",
                "MPI.ReplaceClipPreserveSubClip",
                "MPI.MonitorGrowingFile",
            ):
                run_live_check(name, skip_reason="No imported audio clip")

        if audio_track:
            state = run_live_check(
                "TL.GetVoiceIsolationState",
                lambda: timeline.GetVoiceIsolationState(audio_track),
            )
            safe_state = state if isinstance(state, dict) else {"isEnabled": False, "amount": 0}
            run_live_check(
                "TL.SetVoiceIsolationState",
                lambda: timeline.SetVoiceIsolationState(audio_track, safe_state),
            )
        else:
            run_live_check("TL.GetVoiceIsolationState", skip_reason="No audio track")
            run_live_check("TL.SetVoiceIsolationState", skip_reason="No audio track")

        item_for_name = video_item or audio_item
        if item_for_name:
            original_item_name = item_for_name.GetName()
            run_live_check("TI.SetName", lambda: item_for_name.SetName(f"{original_item_name}_renamed"))
            item_for_name.SetName(original_item_name)
        else:
            run_live_check("TI.SetName", skip_reason="No timeline item")

        if audio_item:
            state = run_live_check("TI.GetVoiceIsolationState", lambda: audio_item.GetVoiceIsolationState())
            safe_state = state if isinstance(state, dict) else {"isEnabled": False, "amount": 0}
            run_live_check("TI.SetVoiceIsolationState", lambda: audio_item.SetVoiceIsolationState(safe_state))
        else:
            run_live_check("TI.GetVoiceIsolationState", skip_reason="No audio timeline item")
            run_live_check("TI.SetVoiceIsolationState", skip_reason="No audio timeline item")

        if video_item:
            run_live_check("TI.ResetAllNodeColors", lambda: video_item.ResetAllNodeColors())
        else:
            run_live_check("TI.ResetAllNodeColors", skip_reason="No video timeline item")

    finally:
        cleanup(pm, original_project_name)

    out_path = os.path.join(os.path.dirname(__file__), "test_resolve20_results.json")
    with open(out_path, "w") as handle:
        json.dump(results, handle, indent=2)

    print("\n=== Results ===")
    print(f"Pass: {len(results['pass'])}")
    print(f"Fail: {len(results['fail'])}")
    print(f"Skip: {len(results['skip'])}")
    print(f"Results saved to {out_path}")

    return 0 if not results["fail"] and not results["skip"] else 1


if __name__ == "__main__":
    sys.exit(main())
