/**
 * Behavioural regression tests for the video tap surface — rendered against the
 * REAL component, not a hand-modelled DOM.
 *
 * TRACES: UR-005, UR-061 | DR-098 | UT-092
 *
 * Why this file exists:
 *
 * `tapGestures.test.ts` tests `registerTap` / `isControlSurfaceTouch` /
 * `isSynthesizedTouchClick` as isolated pure functions. Every one of those tests
 * passed while, on the device, in sequence: the player pause-looped, then
 * pausing became impossible, then the bottom controls went dead, then
 * double-tap-to-seek stopped working. The helpers were each behaving exactly as
 * specified — the bugs were all in the *composition*: which element actually
 * receives a tap once Svelte has re-rendered.
 *
 * Testing my own helpers could not catch that, and modelling the DOM by hand in
 * a test just re-encodes the same wrong assumption. So these tests render
 * VideoPlayer and dispatch real touch/click events at whatever element is
 * genuinely on top, asserting user-visible outcomes ("a double tap seeks")
 * rather than internals.
 *
 * The specific traps encoded here, each a bug that shipped:
 *   - pausing renders a full-screen <button> play overlay OVER the video, so the
 *     second tap of a double tap lands on a button, not the video;
 *   - the browser synthesizes a `click` after a touch tap, which must not toggle
 *     a second time, on ANY layered target;
 *   - the bottom controls bar must drive its own buttons and NOT the container's
 *     tap gestures.
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render } from "@testing-library/svelte";
import { tick } from "svelte";
import VideoPlayer from "./VideoPlayer.svelte";
import { SEEK_FORWARD_SECONDS } from "./tapGestures";

// --- Mocks: everything VideoPlayer reaches for that is not the tap surface. ---

const toggleSpy = vi.fn();
const seekVideoSpy = vi.fn();
const seekSpy = vi.fn();

vi.mock("$app/navigation", () => ({ goto: vi.fn() }));

vi.mock("$lib/player", () => ({
  playerController: {
    toggle: (...a: unknown[]) => {
      toggleSpy(...a);
      return Promise.resolve();
    },
    seekVideo: (...a: unknown[]) => {
      seekVideoSpy(...a);
      return Promise.resolve();
    },
    seek: (...a: unknown[]) => {
      seekSpy(...a);
      return Promise.resolve();
    },
    setActiveAdapter: vi.fn(),
    clearActiveAdapter: vi.fn(),
    getActiveAdapter: vi.fn(() => null),
  },
}));

vi.mock("$lib/player/adapters/rustReportHost", () => ({
  createRustReportHost: () => ({
    onState: vi.fn(),
    onPosition: vi.fn(),
    onMediaLoaded: vi.fn(),
    onEnded: vi.fn(),
    onError: vi.fn(),
    onStreamUrlChanged: vi.fn(),
    onBuffering: vi.fn(),
    onReady: vi.fn(),
  }),
}));

vi.mock("$lib/player/html5Adapter", () => ({
  reportState: vi.fn(),
  reportPosition: vi.fn(),
  reportMediaLoaded: vi.fn(),
  resetReporting: vi.fn(),
}));

vi.mock("$lib/utils/pictureInPicture", () => ({
  isPipSupported: () => false,
  enterPip: vi.fn(),
  setAutoEnterEnabled: vi.fn(),
}));

vi.mock("$lib/stores/auth", () => ({
  auth: {
    getRepository: () => ({ getHandle: () => "h", jrayActorsAt: async () => [] }),
    subscribe: (fn: (v: unknown) => void) => {
      fn({ isAuthenticated: true });
      return () => {};
    },
  },
}));

const MEDIA = {
  id: "item-1",
  name: "Test Episode",
  type: "Episode",
  runTimeTicks: 6_000_000_000, // 600s
} as any;

/** Dispatch a touch at (x, y) on whatever element is topmost there. */
function touchAt(el: Element, x: number) {
  const touch = { clientX: x, clientY: 300 } as Touch;
  el.dispatchEvent(
    new TouchEvent("touchstart", {
      bubbles: true,
      cancelable: true,
      touches: [touch] as unknown as Touch[],
    })
  );
}

function renderPlayer() {
  return render(VideoPlayer, {
    props: { media: MEDIA, streamUrl: "http://x/master.m3u8", onClose: vi.fn() },
  });
}

describe("VideoPlayer tap surface (real component)", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("a single tap on the video toggles play/pause exactly once", async () => {
    const { container } = renderPlayer();
    const video = container.querySelector("video");
    expect(video).toBeTruthy();

    touchAt(video!, 900);

    expect(toggleSpy).toHaveBeenCalledTimes(1);
  });

  it("the synthesized click after a tap does not toggle a second time", async () => {
    const { container } = renderPlayer();
    const video = container.querySelector("video")!;

    touchAt(video, 900);
    // The compatibility click the browser fires after a touch tap. detail=0 is
    // how engines mark it; a late real-detail click is covered by the recency
    // guard, which this exercises too since it lands immediately.
    video.dispatchEvent(new MouseEvent("click", { bubbles: true, detail: 0 }));

    expect(toggleSpy).toHaveBeenCalledTimes(1);
  });

  it("a double tap seeks even though the first tap raised the play overlay", async () => {
    // THE regression this file exists for. On device the first tap pauses, which
    // makes Svelte render a full-screen <button> play overlay over the video —
    // so the SECOND tap lands on a button, not the video. A control-surface
    // guard that does not know about that overlay discards it and seeking dies.
    //
    // Reproducing it requires the overlay to actually render, which means
    // driving `isPlaying` the way the real element does: via its `pause` event.
    vi.useFakeTimers();
    try {
      const { container } = renderPlayer();
      const video = container.querySelector("video")!;

      // Tap 1 on the video.
      touchAt(video, 900);

      // The element reports it paused → isPlaying=false → overlay renders.
      video.dispatchEvent(new Event("pause"));
      await Promise.resolve();
      await tick();

      const overlay = container.querySelector("[data-player-surface]");
      expect(overlay, "the play overlay should be covering the video").toBeTruthy();

      vi.advanceTimersByTime(120); // inside DOUBLE_TAP_WINDOW_MS
      // Tap 2 lands on the OVERLAY, exactly as on device.
      touchAt(overlay!, 900);

      // Either seek route is acceptable — which one runs depends on whether a
      // video adapter is registered. What must hold is that a seek happened, to
      // roughly the forward-skip target.
      const calls = [...seekVideoSpy.mock.calls, ...seekSpy.mock.calls];
      expect(calls.length).toBe(1);
      const [position] = calls[0];
      expect(position).toBeGreaterThan(0);
      expect(position).toBeLessThanOrEqual(SEEK_FORWARD_SECONDS);
    } finally {
      vi.useRealTimers();
    }
  });

  it("tapping the bottom play/pause button toggles once, not twice", async () => {
    const { container } = renderPlayer();
    const controls = container.querySelector("[data-player-controls]");
    expect(controls).toBeTruthy();

    const playBtn = controls!.querySelector("button");
    expect(playBtn).toBeTruthy();

    // A real press: touchstart bubbles to the container's gesture handler, then
    // the button's own click fires. Only ONE toggle may result.
    touchAt(playBtn!, 40);
    playBtn!.dispatchEvent(new MouseEvent("click", { bubbles: true, detail: 1 }));

    expect(toggleSpy).toHaveBeenCalledTimes(1);
  });
});
