import { describe, expect, it, vi } from "vitest";

import { handleRuntimeMessage, type MessageHandlerCallbacks } from "./runtime-message-handler.js";
import type { ParentMediaManager } from "./parent-media.js";
import type { ShaderLoaderState } from "./shader-loader-state.js";

// Only the stage-size branch is exercised here; the rest of the callback
// surface is satisfied with inert spies so the handler's type contract
// stays honest without pulling in the real player.
const makeCallbacks = (): MessageHandlerCallbacks => ({
  updateControlsTime: vi.fn(),
  updateControlsPlaying: vi.fn(),
  dispatchEvent: vi.fn(),
  onRuntimeReady: vi.fn(),
  onRuntimeTimelineReady: vi.fn(),
  seek: vi.fn(),
  play: vi.fn(),
  getLoop: vi.fn(() => false),
  media: { mirrorTime: vi.fn(), promoteToParentProxy: vi.fn() } as unknown as ParentMediaManager,
  getPlaybackState: vi.fn(() => ({ currentTime: 0, duration: 0, paused: true, lastUpdateMs: 0 })),
  setPlaybackState: vi.fn(),
  setScenes: vi.fn(),
  getShaderLoadingMode: vi.fn(() => "auto"),
  shaderLoader: { update: vi.fn() } as unknown as ShaderLoaderState,
  setCompositionSize: vi.fn(),
  sendControl: vi.fn(),
  getIframeDoc: vi.fn(() => null),
});

const stageSizeEvent = (width: unknown, height: unknown, source: object): MessageEvent =>
  ({
    source,
    data: { source: "hf-preview", type: "stage-size", width, height },
  }) as unknown as MessageEvent;

describe("handleRuntimeMessage stage-size", () => {
  it("applies a finite positive stage size", () => {
    const frameWindow = {} as Window;
    const callbacks = makeCallbacks();

    handleRuntimeMessage(stageSizeEvent(1280, 720, frameWindow), frameWindow, callbacks);

    expect(callbacks.setCompositionSize).toHaveBeenCalledWith(1280, 720);
  });

  it.each([
    ["Infinity width", Infinity, 720],
    ["Infinity height", 1280, Infinity],
    ["NaN width", NaN, 720],
    ["zero width", 0, 720],
    ["negative height", 1280, -720],
    ["string width", "1280", 720],
  ])("ignores stage-size with %s", (_label, width, height) => {
    const frameWindow = {} as Window;
    const callbacks = makeCallbacks();

    handleRuntimeMessage(stageSizeEvent(width, height, frameWindow), frameWindow, callbacks);

    expect(callbacks.setCompositionSize).not.toHaveBeenCalled();
  });

  it("ignores messages from a different source window", () => {
    const frameWindow = {} as Window;
    const callbacks = makeCallbacks();

    handleRuntimeMessage(stageSizeEvent(1280, 720, {}), frameWindow, callbacks);

    expect(callbacks.setCompositionSize).not.toHaveBeenCalled();
  });
});

describe("handleRuntimeMessage media autoplay fallback", () => {
  const autoplayBlockedEvent = (source: object): MessageEvent =>
    ({
      source,
      data: { source: "hf-preview", type: "media-autoplay-blocked" },
    }) as unknown as MessageEvent;

  it("promotes and mutes iframe output by default", () => {
    const frameWindow = {} as Window;
    const callbacks = makeCallbacks();

    handleRuntimeMessage(autoplayBlockedEvent(frameWindow), frameWindow, callbacks);

    expect(callbacks.media.promoteToParentProxy).toHaveBeenCalled();
    expect(callbacks.sendControl).toHaveBeenCalledWith("set-media-output-muted", {
      muted: true,
    });
  });

  it("does not promote or mute iframe output when the host vetoes the fallback", () => {
    const frameWindow = {} as Window;
    const callbacks = {
      ...makeCallbacks(),
      shouldPromoteMediaAutoplayFallback: vi.fn(() => false),
    };

    handleRuntimeMessage(autoplayBlockedEvent(frameWindow), frameWindow, callbacks);

    expect(callbacks.shouldPromoteMediaAutoplayFallback).toHaveBeenCalled();
    expect(callbacks.media.promoteToParentProxy).not.toHaveBeenCalled();
    expect(callbacks.sendControl).not.toHaveBeenCalled();
  });
});

describe("handleRuntimeMessage timeline ready", () => {
  const timelineEvent = (durationInFrames: unknown, source: object): MessageEvent =>
    ({
      source,
      data: { source: "hf-preview", type: "timeline", durationInFrames, scenes: [] },
    }) as unknown as MessageEvent;

  it("reports a finite positive timeline duration in seconds", () => {
    const frameWindow = {} as Window;
    const callbacks = makeCallbacks();

    handleRuntimeMessage(timelineEvent(120, frameWindow), frameWindow, callbacks);

    expect(callbacks.onRuntimeTimelineReady).toHaveBeenCalledWith(4);
  });

  it("does not report invalid timeline durations as ready", () => {
    const frameWindow = {} as Window;
    const callbacks = makeCallbacks();

    handleRuntimeMessage(timelineEvent(Infinity, frameWindow), frameWindow, callbacks);

    expect(callbacks.onRuntimeTimelineReady).not.toHaveBeenCalled();
  });
});
