import { describe, expect, it } from 'vitest';
import type { Pool } from 'pg';
import {
  NotSessionOwnerError,
  PlaybackEvent,
  PlaybackSyncService,
} from './playback-sync.service.js';

const USER_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
const DESKTOP = '11111111-1111-4111-8111-111111111111';
const PHONE = '22222222-2222-4222-8222-222222222222';

/**
 * A pool stubbed down to the one row this service reasons about. The queries
 * themselves are plain SQL against a single table, so what is worth testing is
 * the ownership and fan-out logic sitting on top of them.
 */
function poolWith(state: {
  deviceId?: string | null;
  trackId?: string | null;
  isPlaying?: boolean;
  positionMs?: number;
}) {
  const row = {
    device_id: state.deviceId ?? null,
    track_id: state.trackId ?? null,
    queue: [],
    queue_index: -1,
    position_ms: state.positionMs ?? 0,
    is_playing: state.isPlaying ?? false,
    version: '4',
    updated_at: new Date('2026-08-08T12:00:00Z'),
  };
  const queries: string[] = [];
  const pool = {
    async query(rawSql: string, params?: unknown[]) {
      queries.push(rawSql);
      // The service formats its SQL in columns, so match on collapsed text.
      const sql = rawSql.replace(/\s+/g, ' ').trim();
      if (sql.includes('FROM playback_devices') && sql.includes('ORDER BY')) {
        return { rows: [{ id: DESKTOP, name: 'Linux · Firefox', last_seen_at: row.updated_at, online: true }], rowCount: 1 };
      }
      if (sql.startsWith('SELECT device_id FROM playback_state')) {
        return { rows: [{ device_id: row.device_id }], rowCount: 1 };
      }
      if (sql.includes('SELECT id FROM playback_devices')) {
        return { rows: [{ id: params?.[0] }], rowCount: 1 };
      }
      if (sql.includes('UPDATE playback_state') && sql.includes('device_id = $2')) {
        row.device_id = String(params?.[1]);
        return { rows: [row], rowCount: 1 };
      }
      if (sql.includes('UPDATE playback_state')) {
        return { rows: [row], rowCount: 1 };
      }
      return { rows: [row], rowCount: 1 };
    },
  } as unknown as Pool;
  return { pool, queries, row };
}

describe('cross-device playback', () => {
  it('refuses a state report from a device that no longer holds the audio', async () => {
    const { pool } = poolWith({ deviceId: DESKTOP });
    const service = new PlaybackSyncService(pool);

    await expect(service.reportState(USER_ID, PHONE, { isPlaying: true }))
      .rejects.toBeInstanceOf(NotSessionOwnerError);
  });

  it('accepts the first device to report on an unowned session', async () => {
    const { pool } = poolWith({ deviceId: null });
    const service = new PlaybackSyncService(pool);

    const state = await service.reportState(USER_ID, PHONE, { isPlaying: true });
    expect(state.deviceId).toBe(PHONE);
  });

  it('delivers a command to the owning device only', async () => {
    const { pool } = poolWith({ deviceId: DESKTOP });
    const service = new PlaybackSyncService(pool);
    const seen: PlaybackEvent[] = [];
    service.subscribe(USER_ID, (event) => seen.push(event));

    const result = await service.sendCommand(USER_ID, { type: 'pause' });

    expect(result.deliveredTo).toBe(DESKTOP);
    expect(seen).toContainEqual({ type: 'command', deviceId: DESKTOP, command: { type: 'pause' } });
  });

  it('reports no delivery when nothing holds the audio', async () => {
    const { pool } = poolWith({ deviceId: null });
    const service = new PlaybackSyncService(pool);

    expect(await service.sendCommand(USER_ID, { type: 'play' })).toEqual({ deliveredTo: null });
  });

  it('hands the session to another device without losing the position', async () => {
    const { pool } = poolWith({ deviceId: DESKTOP, trackId: 'track-1', positionMs: 45_000, isPlaying: true });
    const service = new PlaybackSyncService(pool);
    const seen: PlaybackEvent[] = [];
    service.subscribe(USER_ID, (event) => seen.push(event));

    const state = await service.transfer(USER_ID, PHONE);

    expect(state.deviceId).toBe(PHONE);
    expect(state.position).toBe(45);
    expect(state.isPlaying).toBe(true);
    expect(seen.some((event) => event.type === 'state')).toBe(true);
  });

  it('frees ownership when the owning device goes away', async () => {
    const { pool, queries } = poolWith({ deviceId: DESKTOP });
    const service = new PlaybackSyncService(pool);

    await service.releaseIfOwner(USER_ID, DESKTOP);

    expect(queries.some((sql) => sql.includes('SET device_id = NULL'))).toBe(true);
  });
});
