import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { flushSync } from 'svelte';

vi.mock('$lib/api/client', () => ({
  api: {
    post: vi.fn().mockResolvedValue({ play_event_id: 'pe-1', session_id: 's-1' })
  }
}));

vi.mock('./clientId', () => ({
  getOrCreateClientId: () => 'test-client'
}));

import { useEventsDispatcher } from './events.svelte';
import {
  playQueue,
  reportStateFromAudio,
  reportTimeUpdate,
  reportDuration,
  skipNext,
} from './store.svelte';
import { api } from '$lib/api/client';
import type { TrackRef } from '$lib/api/types';

function track(id: string, dur = 200): TrackRef {
  return {
    id, title: `T ${id}`,
    album_id: 'a', album_title: 'A',
    artist_id: 'ar', artist_name: 'Ar',
    track_number: Number(id), disc_number: 1,
    duration_sec: dur,
    stream_url: `/api/tracks/${id}/stream`
  };
}

beforeEach(() => {
  vi.clearAllMocks();
  // Reset the player store to idle.
  playQueue([]);
});

afterEach(() => {
  // sendBeacon spies are restored automatically per test.
});

describe('useEventsDispatcher', () => {
  test('first transition into playing for a new track POSTs play_started', async () => {
    const cleanup = $effect.root(() => useEventsDispatcher());

    playQueue([track('1')]);
    flushSync();
    reportStateFromAudio('playing');
    flushSync();
    // Microtask drain so the api.post promise settles.
    await Promise.resolve();
    await Promise.resolve();

    const calls = (api.post as ReturnType<typeof vi.fn>).mock.calls;
    const startedCalls = calls.filter((c) => (c[1] as { type: string }).type === 'play_started');
    expect(startedCalls.length).toBe(1);
    expect(startedCalls[0][0]).toBe('/api/events');
    expect(startedCalls[0][1]).toMatchObject({
      type: 'play_started',
      track_id: '1',
      client_id: 'test-client'
    });

    cleanup();
  });

  test('track end (duration reached, paused) POSTs play_ended', async () => {
    const cleanup = $effect.root(() => useEventsDispatcher());

    playQueue([track('1', 200)]);
    flushSync();
    reportDuration(200);
    reportStateFromAudio('playing');
    flushSync();
    await Promise.resolve();
    await Promise.resolve();
    // Reach end of track, then natural pause.
    reportTimeUpdate(200);
    flushSync();
    reportStateFromAudio('ended');
    flushSync();
    await Promise.resolve();

    const calls = (api.post as ReturnType<typeof vi.fn>).mock.calls;
    const types = calls.map((c) => (c[1] as { type: string }).type);
    expect(types).toContain('play_started');
    expect(types).toContain('play_ended');

    cleanup();
  });

  test('user-initiated skip mid-track POSTs play_skipped', async () => {
    const cleanup = $effect.root(() => useEventsDispatcher());

    playQueue([track('1'), track('2')]);
    flushSync();
    reportStateFromAudio('playing');
    flushSync();
    await Promise.resolve();
    await Promise.resolve();
    skipNext();
    flushSync();
    await Promise.resolve();

    const calls = (api.post as ReturnType<typeof vi.fn>).mock.calls;
    const types = calls.map((c) => (c[1] as { type: string }).type);
    expect(types).toContain('play_skipped');

    cleanup();
  });

  test('pagehide fires sendBeacon when a play is open', async () => {
    // jsdom doesn't ship navigator.sendBeacon — install a stub before spy.
    if (typeof navigator.sendBeacon !== 'function') {
      Object.defineProperty(navigator, 'sendBeacon', {
        value: () => true,
        configurable: true
      });
    }
    const cleanup = $effect.root(() => useEventsDispatcher());
    const beacon = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true);

    playQueue([track('1')]);
    flushSync();
    reportStateFromAudio('playing');
    flushSync();
    await Promise.resolve();
    await Promise.resolve();

    window.dispatchEvent(new Event('pagehide'));

    expect(beacon).toHaveBeenCalled();
    expect(beacon.mock.calls[0][0]).toBe('/api/events');

    cleanup();
    beacon.mockRestore();
  });
});
