/**
 * VideoPlayer seek-bar TOUCH scrub regression tests (Android).
 *
 * Reported bug: on Android, dragging the progress bar does not change the
 * playback location.
 *
 * The gesture listener lives on the outer container and touch events bubble.
 * `handleTouchStart` ignores touches that land on a control (the seek bar is an
 * <input>, inside `data-player-controls`) — but `handleTouchMove` does not, so a
 * seek-bar drag is still interpreted as a container swipe. That mis-read swipe
 * fires `togglePlayPause()` (undoing a first-tap toggle that never happened) and
 * hijacks the drag into brightness control.
 *
 * The existing scrub regression tests only drive the slider with MOUSE events,
 * which never reach the touch handlers — which is why this survived.
 *
 * The seek was also committed only from `change`, which Android's WebView does
 * not reliably fire for a touch interaction on a range input — so a tap moved
 * the thumb and no seek ever ran. Release now commits from touchend/mouseup too.
 *
 * TRACES: UR-005, UR-061 | DR-098, DR-099 | UT-089, UT-090
 */

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

// ---- Mocks (must precede component import) --------------------------------

const channelHandlers: Record<string, (event: any) => void> = {};
vi.mock("@tauri-apps/api/event", () => ({
  listen: vi.fn(async (channel: string, handler: any) => {
    channelHandlers[channel] = handler;
    return () => {
      delete channelHandlers[channel];
    };
  }),
}));

vi.mock("@tauri-apps/api/core", () => ({
  invoke: vi.fn(),
}));

const playerPlayItem = vi.fn(async () => ({
  useHtml5Element: false,
  backend: "exoplayer",
  state: { kind: "playing" },
}));
const playerSeekVideo = vi.fn(async (_h: string, position: number) => ({
  strategy: "native",
  position,
}));
const playerStop = vi.fn(async () => ({}));
const playerToggle = vi.fn(async () => ({ state: "playing" }));

vi.mock("$lib/api/bindings", () => ({
  commands: {
    playerPlayItem: (...a: any[]) => playerPlayItem(...(a as [])),
    playerSeekVideo: (...a: any[]) => playerSeekVideo(...(a as [string, number])),
    playerStop: (...a: any[]) => playerStop(...(a as [])),
    playerToggle: (...a: any[]) => playerToggle(...(a as [])),
    playerPlay: vi.fn(async () => ({})),
    playerPause: vi.fn(async () => ({})),
    playerSetSleepTimer: vi.fn(async () => ({})),
    playerCancelSleepTimer: vi.fn(async () => ({})),
    playerSetSubtitleTrack: vi.fn(async () => ({})),
    playerSwitchAudioTrack: vi.fn(async () => ({})),
    storageGetSeriesAudioPreference: vi.fn(async () => null),
    storageSaveSeriesAudioPreference: vi.fn(async () => ({})),
  },
  events: {
    playerStatusEvent: { listen: vi.fn(async () => () => {}) },
  },
}));

vi.mock("$lib/stores/auth", () => ({
  auth: {
    getUserId: () => "user-1",
    getRepository: () => ({
      getHandle: () => "repo-1",
      getSubtitleUrl: async () => "",
      jrayActorsAt: async () => [],
    }),
  },
}));

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

import { render, fireEvent, waitFor } from "@testing-library/svelte";
import { tick } from "svelte";
import VideoPlayer from "./VideoPlayer.svelte";
import type { MediaItem } from "$lib/api/types";

function makeEpisode(): MediaItem {
  return {
    id: "ep1",
    name: "Episode 1",
    kind: "episode",
    durationMs: 24 * 60 * 1000, // 24 min
  } as MediaItem;
}

async function mountAndroidPlayer() {
  const utils = render(VideoPlayer, {
    props: {
      media: makeEpisode(),
      streamUrl: "http://server/videos/ep1/master.m3u8",
      mediaSourceId: "src-1",
      needsTranscoding: false,
      onClose: vi.fn(),
    },
  });

  await waitFor(() => expect(playerPlayItem).toHaveBeenCalled());
  await waitFor(() => expect(playerStop).toHaveBeenCalled());

  const slider = utils.container.querySelector(
    'input[type="range"]'
  ) as HTMLInputElement;
  const video = utils.container.querySelector("video") as HTMLVideoElement;
  expect(slider).not.toBeNull();
  return { ...utils, slider, video };
}

function touch(x: number, y: number) {
  return { clientX: x, clientY: y } as Touch;
}

/**
 * Drag the seek bar with TOUCH events, the way a finger does on Android.
 *
 * A real drag along the bar moves the finger far enough that the container's
 * swipe detector (50px) would trigger if it were still listening.
 */
async function touchScrubTo(
  slider: HTMLInputElement,
  video: HTMLVideoElement,
  target: number
) {
  await fireEvent.touchStart(slider, { touches: [touch(100, 700)] });
  // Finger travels across the bar. Small vertical wander is normal for a thumb
  // drag; the horizontal travel is what matters.
  await fireEvent.touchMove(slider, { touches: [touch(400, 690)] });
  slider.value = String(target);
  await fireEvent.input(slider);
  await fireEvent.touchMove(slider, { touches: [touch(700, 705)] });
  await fireEvent.change(slider);
  await fireEvent.touchEnd(slider, { touches: [] });
  if (video) await fireEvent(video, new Event("seeked"));
  await tick();
}

describe("VideoPlayer seek bar — touch drag (Android)", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    for (const key of Object.keys(channelHandlers)) delete channelHandlers[key];
  });

  it("a touch drag on the seek bar seeks to the dragged position", async () => {
    const { slider, video } = await mountAndroidPlayer();

    await touchScrubTo(slider, video, 600);

    await waitFor(() =>
      expect(playerSeekVideo).toHaveBeenCalledWith("repo-1", 600, "src-1", null, true)
    );
    expect(parseFloat(slider.value)).toBeCloseTo(600);
  });

  it("a touch drag on the seek bar never toggles play/pause", async () => {
    const { slider, video } = await mountAndroidPlayer();

    await touchScrubTo(slider, video, 600);

    // The container gesture layer must stay out of a control drag entirely:
    // no swipe mis-read, so no play/pause correction.
    expect(playerToggle).not.toHaveBeenCalled();
  });

  it("commits the seek on touchend even when the engine never fires `change`", async () => {
    const { slider, video } = await mountAndroidPlayer();

    // Android's WebView does not reliably fire `change` for a touch interaction
    // on a range input. A tap on the track still moves the thumb and fires
    // `input` — the seek must be committed on release regardless.
    await fireEvent.touchStart(slider, { touches: [touch(400, 700)] });
    slider.value = "600";
    await fireEvent.input(slider);
    await fireEvent.touchEnd(slider, { touches: [] });
    if (video) await fireEvent(video, new Event("seeked"));
    await tick();

    await waitFor(() =>
      expect(playerSeekVideo).toHaveBeenCalledWith("repo-1", 600, "src-1", null, true)
    );
  });

  it("commits the seek exactly once when both touchend and change fire", async () => {
    const { slider, video } = await mountAndroidPlayer();

    await touchScrubTo(slider, video, 600);

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

  it("a touch drag on the seek bar does not hijack into brightness control", async () => {
    const { slider, video, container } = await mountAndroidPlayer();

    await touchScrubTo(slider, video, 600);

    // Brightness is applied as a CSS filter on the <video>; a control drag must
    // leave it untouched.
    const el = container.querySelector("video") as HTMLVideoElement | null;
    if (el) {
      expect(el.style.filter).toBe("brightness(1)");
    }
  });
});
