import { afterEach, beforeEach, describe, expect, test, vi } 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}`,
    stream_url: `/api/tracks/${id}/stream`
  });
}

type Handler = (details: MediaSessionActionDetails) => void;

// Spy MediaSession replaces navigator.mediaSession for the duration of each test.
function makeSpyMediaSession() {
  const handlers: Record<string, Handler> = {};
  const spy = {
    metadata: null as MediaMetadata | null,
    playbackState: 'none' as MediaSessionPlaybackState,
    setActionHandler: vi.fn((action: string, h: Handler | null) => {
      if (h) handlers[action] = h;
      else delete handlers[action];
    }),
    setPositionState: vi.fn(),
    handlers
  };
  return spy;
}

describe('useMediaSession', () => {
  let spy: ReturnType<typeof makeSpyMediaSession>;
  let originalMS: unknown;

  beforeEach(async () => {
    spy = makeSpyMediaSession();
    originalMS = (navigator as unknown as { mediaSession?: unknown }).mediaSession;
    Object.defineProperty(navigator, 'mediaSession', {
      configurable: true,
      value: spy
    });
    // Also stub MediaMetadata so we can construct it in jsdom.
    (globalThis as unknown as { MediaMetadata: typeof Object }).MediaMetadata =
      class { constructor(public init: unknown) {} } as unknown as typeof Object;

    const store = await import('./store.svelte');
    store.playQueue([]);
  });

  afterEach(() => {
    if (originalMS === undefined) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      delete (navigator as any).mediaSession;
    } else {
      Object.defineProperty(navigator, 'mediaSession', {
        configurable: true,
        value: originalMS
      });
    }
    vi.restoreAllMocks();
  });

  test('sets metadata when current track changes; clears when queue empties', async () => {
    const store = await import('./store.svelte');
    const { useMediaSession } = await import('./mediaSession.svelte');
    const cleanup = $effect.root(() => useMediaSession());

    store.playQueue([track('1')]);
    flushSync();
    expect(spy.metadata).not.toBeNull();

    store.playQueue([]);
    flushSync();
    expect(spy.metadata).toBeNull();

    cleanup();
  });

  test('registers play/pause/previoustrack/nexttrack/seekto handlers', async () => {
    const { useMediaSession } = await import('./mediaSession.svelte');
    const cleanup = $effect.root(() => useMediaSession());
    flushSync();

    expect(spy.handlers['play']).toBeTypeOf('function');
    expect(spy.handlers['pause']).toBeTypeOf('function');
    expect(spy.handlers['previoustrack']).toBeTypeOf('function');
    expect(spy.handlers['nexttrack']).toBeTypeOf('function');
    expect(spy.handlers['seekto']).toBeTypeOf('function');

    cleanup();
  });

  test('playbackState mirrors player.isPlaying', async () => {
    const store = await import('./store.svelte');
    const { useMediaSession } = await import('./mediaSession.svelte');
    const cleanup = $effect.root(() => useMediaSession());

    store.playQueue([track('1')]);
    store.reportStateFromAudio('playing');
    flushSync();
    expect(spy.playbackState).toBe('playing');

    store.reportStateFromAudio('paused');
    flushSync();
    expect(spy.playbackState).toBe('paused');

    cleanup();
  });

  test('setPositionState called when duration > 0', async () => {
    const store = await import('./store.svelte');
    const { useMediaSession } = await import('./mediaSession.svelte');
    const cleanup = $effect.root(() => useMediaSession());

    store.playQueue([track('1')]);
    store.reportDuration(240);
    store.reportTimeUpdate(30);
    flushSync();

    expect(spy.setPositionState).toHaveBeenCalled();
    const call = spy.setPositionState.mock.calls.at(-1)?.[0];
    expect(call?.duration).toBe(240);
    expect(call?.position).toBeLessThanOrEqual(240);

    cleanup();
  });
});
