import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { flushSync } from 'svelte';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';

function track(id: string): TrackRef {
  return makeTrack({
    id,
    title: `T ${id}`,
    duration_sec: 200,
    stream_url: `/api/tracks/${id}/stream`
  });
}

function press(key: string, target?: EventTarget): void {
  const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true });
  (target ?? window).dispatchEvent(event);
}

describe('useGlobalShortcuts', () => {
  let cleanup: () => void;

  beforeEach(async () => {
    const store = await import('./store.svelte');
    store.playQueue([track('1'), track('2'), track('3')]);
    store.reportDuration(200);
    flushSync();
    const { useGlobalShortcuts } = await import('./shortcuts.svelte');
    cleanup = $effect.root(() => useGlobalShortcuts());
  });

  afterEach(() => {
    cleanup?.();
  });

  test('Space toggles play/pause', async () => {
    const store = await import('./store.svelte');
    // playQueue leaves the store at 'loading' (intent-to-play; jsdom
    // never moves it to 'playing' because there's no real audio). The
    // togglePlay contract is: loading|playing -> paused, anything else
    // -> loading. So Space round-trips loading <-> paused.
    expect(store.player.state).toBe('loading');
    press(' ');
    flushSync();
    expect(store.player.state).toBe('paused');
    press(' ');
    flushSync();
    expect(store.player.state).toBe('loading');
  });

  test('K is an alias for Space (YouTube convention)', async () => {
    const store = await import('./store.svelte');
    expect(store.player.state).toBe('loading');
    press('k');
    flushSync();
    expect(store.player.state).toBe('paused');
  });

  test('ArrowRight advances to the next track', async () => {
    const store = await import('./store.svelte');
    expect(store.player.index).toBe(0);
    press('ArrowRight');
    flushSync();
    expect(store.player.index).toBe(1);
  });

  test('ArrowLeft goes to the previous track', async () => {
    const store = await import('./store.svelte');
    store.skipNext();
    flushSync();
    expect(store.player.index).toBe(1);
    press('ArrowLeft');
    flushSync();
    expect(store.player.index).toBe(0);
  });

  test('M toggles mute, restoring the previous volume', async () => {
    const store = await import('./store.svelte');
    store.setVolume(0.42);
    flushSync();
    expect(store.player.volume).toBe(0.42);
    press('m');
    flushSync();
    expect(store.player.volume).toBe(0);
    press('m');
    flushSync();
    expect(store.player.volume).toBeCloseTo(0.42, 5);
  });

  test('ArrowUp / ArrowDown adjust volume by 5%', async () => {
    const store = await import('./store.svelte');
    store.setVolume(0.5);
    flushSync();
    press('ArrowUp');
    flushSync();
    expect(store.player.volume).toBeCloseTo(0.55, 5);
    press('ArrowDown');
    flushSync();
    expect(store.player.volume).toBeCloseTo(0.5, 5);
  });

  test('shortcuts are ignored when focus is in an input', async () => {
    const store = await import('./store.svelte');
    const before = store.player.state;
    const input = document.createElement('input');
    document.body.appendChild(input);
    input.focus();
    press(' ', input);
    flushSync();
    // No state change — Space should pass through to the input.
    expect(store.player.state).toBe(before);
    input.remove();
  });

  test('modifier keys disable the shortcut (browser hotkey passthrough)', async () => {
    const store = await import('./store.svelte');
    const before = store.player.state;
    const event = new KeyboardEvent('keydown', {
      key: ' ',
      ctrlKey: true,
      bubbles: true,
      cancelable: true
    });
    window.dispatchEvent(event);
    flushSync();
    expect(store.player.state).toBe(before);
  });
});
