import { renderHook, act, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { usePlaybackStore } from '../store/usePlaybackStore';
import { useVibeStore } from '../store/useVibeStore';
import { playbackSyncService, type PlaybackSnapshot, type PlaybackSyncEvent } from '../services/playbackSync';
import { usePlaybackSync } from './usePlaybackSync';

const THIS_DEVICE = '11111111-1111-1111-1111-111111111111';
const OTHER_DEVICE = '22222222-2222-2222-2222-222222222222';

let emit: ((event: PlaybackSyncEvent) => void) | null = null;

const { adoptVibeSession, releaseVibeDriving } = vi.hoisted(() => ({
  adoptVibeSession: vi.fn(async () => true),
  releaseVibeDriving: vi.fn(),
}));
vi.mock('../services/vibeSession', () => ({ adoptVibeSession, releaseVibeDriving }));

vi.mock('../services/playbackSync', () => ({
  storedDeviceId: () => null,
  playbackSyncService: {
    register: vi.fn(async () => ({
      id: THIS_DEVICE,
      name: 'Test device',
      lastSeenAt: new Date(0).toISOString(),
      online: true,
      isOwner: false,
    })),
    reportState: vi.fn(async () => undefined),
    transfer: vi.fn(async () => undefined),
    release: vi.fn(async () => undefined),
    sendCommand: vi.fn(async () => undefined),
    openStream: (_deviceId: string, onEvent: (event: PlaybackSyncEvent) => void) => {
      emit = onEvent;
      return () => { emit = null; };
    },
  },
}));

function snapshot(over: Partial<PlaybackSnapshot> = {}): PlaybackSnapshot {
  return {
    deviceId: THIS_DEVICE,
    trackId: null,
    vibeSessionId: null,
    queue: [],
    queueIndex: 0,
    position: 0,
    isPlaying: true,
    version: 1,
    updatedAt: new Date(0).toISOString(),
    ...over,
  };
}

async function mountOwning() {
  const view = renderHook(() => usePlaybackSync());
  await waitFor(() => expect(emit).not.toBeNull());
  // First snapshot: this device takes the session over at 30s.
  await act(async () => {
    emit!({ type: 'state', state: snapshot({ position: 30, version: 1 }), devices: [] });
  });
  await waitFor(() => expect(view.result.current.isOwner).toBe(true));
  return view;
}

describe('usePlaybackSync', () => {
  beforeEach(() => {
    emit = null;
    useVibeStore.getState().reset();
    usePlaybackStore.setState({
      position: 0, isPlaying: false, queue: [], currentTrack: null, audioElsewhere: false,
      queueOwner: 'ordinary', vibeAdvanceHandler: null,
    });
  });

  it('takes the reported position when it first gains the session', async () => {
    await mountOwning();
    expect(usePlaybackStore.getState().position).toBe(30);
  });

  it('ignores snapshots echoing its own stale position while it owns the audio', async () => {
    await mountOwning();

    // Playback has moved on locally; the server still holds the last report.
    act(() => usePlaybackStore.getState().setPosition(48));
    await act(async () => {
      emit!({ type: 'state', state: snapshot({ position: 30, version: 2 }), devices: [] });
    });

    expect(usePlaybackStore.getState().position).toBe(48);
  });

  it('pauses and follows along once another device takes the session', async () => {
    const view = await mountOwning();
    act(() => usePlaybackStore.setState({ isPlaying: true, position: 48 }));

    await act(async () => {
      emit!({
        type: 'state',
        state: snapshot({ deviceId: OTHER_DEVICE, position: 55, version: 3 }),
        devices: [],
      });
    });

    expect(usePlaybackStore.getState().isPlaying).toBe(false);
    expect(usePlaybackStore.getState().position).toBe(55);
    expect(view.result.current.hasRemoteOwner).toBe(true);
  });

  it('reports the remote play state rather than its own while it watches', async () => {
    const view = await mountOwning();

    await act(async () => {
      emit!({
        type: 'state',
        state: snapshot({ deviceId: OTHER_DEVICE, isPlaying: true, version: 3 }),
        devices: [],
      });
    });

    expect(view.result.current.remotePlaying).toBe(true);
    expect(usePlaybackStore.getState().isPlaying).toBe(false);
    expect(usePlaybackStore.getState().audioElsewhere).toBe(true);
  });

  it('reports the Vibe session it is driving so the next device can take it', async () => {
    await mountOwning();
    act(() => {
      useVibeStore.getState().setActiveSession({ sessionId: 'session-a', seedTrackId: 'track-1' });
      usePlaybackStore.getState().setVibeQueue([{ id: 'track-1' }] as never);
      usePlaybackStore.getState().setCurrentTrack({ id: 'track-1' } as never);
    });

    await waitFor(() => expect(playbackSyncService.reportState).toHaveBeenCalledWith(
      THIS_DEVICE,
      expect.objectContaining({ vibeSessionId: 'session-a' }),
    ));
  });

  it('stops driving the Vibe when the audio moves away, and adopts it back', async () => {
    const view = await mountOwning();
    act(() => {
      usePlaybackStore.getState().setVibeQueue([{ id: 'track-1' }] as never);
      usePlaybackStore.getState().setCurrentTrack({ id: 'track-1' } as never);
      usePlaybackStore.getState().setVibeAdvanceHandler(() => undefined);
      useVibeStore.getState().setActiveSession({ sessionId: 'session-a', seedTrackId: 'track-1' });
    });

    await act(async () => {
      emit!({
        type: 'state',
        state: snapshot({ deviceId: OTHER_DEVICE, vibeSessionId: 'session-a', version: 3 }),
        devices: [],
      });
    });

    // One driver at a time, and it is whichever device holds the audio.
    expect(releaseVibeDriving).toHaveBeenCalled();
    expect(adoptVibeSession).not.toHaveBeenCalled();

    await act(async () => {
      emit!({
        type: 'state',
        state: snapshot({ deviceId: THIS_DEVICE, vibeSessionId: 'session-a', version: 4 }),
        devices: [],
      });
    });

    expect(view.result.current.isOwner).toBe(true);
    expect(usePlaybackStore.getState().audioElsewhere).toBe(false);
    expect(adoptVibeSession).toHaveBeenCalledWith('session-a');
  });

  it('lets go of its own Vibe when it takes over playback that is not one', async () => {
    await mountOwning();
    act(() => {
      usePlaybackStore.getState().setVibeQueue([{ id: 'track-1' }] as never);
      useVibeStore.getState().setActiveSession({ sessionId: 'session-a', seedTrackId: 'track-1' });
    });

    // The other device played an album, and this one is taking that over.
    await act(async () => {
      emit!({
        type: 'state',
        state: snapshot({ deviceId: OTHER_DEVICE, vibeSessionId: null, version: 3 }),
        devices: [],
      });
    });
    await act(async () => {
      emit!({ type: 'state', state: snapshot({ deviceId: THIS_DEVICE, vibeSessionId: null, version: 4 }), devices: [] });
    });

    expect(adoptVibeSession).not.toHaveBeenCalled();
    expect(useVibeStore.getState().activeSessionId).toBeNull();
  });

  it('keeps playing and reclaims the session when a dropped stream leaves it unowned', async () => {
    await mountOwning();
    act(() => usePlaybackStore.setState({
      isPlaying: true,
      position: 48,
      currentTrack: { id: 'track-1' } as never,
    }));

    // What the server publishes after this device's stream breaks: nobody owns
    // the audio, and the position is this device's own report from 18s ago.
    await act(async () => {
      emit!({ type: 'state', state: snapshot({ deviceId: null, position: 30, version: 4 }), devices: [] });
    });

    expect(usePlaybackStore.getState().isPlaying).toBe(true);
    expect(usePlaybackStore.getState().position).toBe(48);
    expect(playbackSyncService.reportState).toHaveBeenCalled();
  });
});
