import { vi, type Mock } from 'vitest';
import type { RealtimeModel, RealtimeServerEvent } from '../../types/realtime-model';

export class FakeTrack extends EventTarget {
  enabled = true;
  muted = false;
  readyState = 'live';
  stop = vi.fn(() => { this.readyState = 'ended'; });
}

export function fakeStream(tracks = [new FakeTrack()]) {
  return { track: tracks[0], stream: { getTracks: () => tracks, getAudioTracks: () => tracks } as unknown as MediaStream };
}

export class FakeDataChannel {
  readyState = 'connecting';
  bufferedAmount = 0;
  onopen: (() => void) | null = null;
  onclose: (() => void) | null = null;
  onerror: (() => void) | null = null;
  onmessage: ((event: { data: unknown }) => void) | null = null;
  sent: Array<{ type: string }> = [];
  send = vi.fn((data: string) => { this.sent.push(JSON.parse(data)); });
  close = vi.fn(() => { this.readyState = 'closed'; this.onclose?.(); });
  emit(event: RealtimeServerEvent) { this.onmessage?.({ data: JSON.stringify(event) }); }
  open() { this.readyState = 'open'; this.onopen?.(); }
}

export class FakePeerConnection {
  static instances: FakePeerConnection[] = [];
  static autoStart = true;
  static autoOpen = true;
  static gather = true;
  dc = new FakeDataChannel();
  iceGatheringState = FakePeerConnection.gather ? 'complete' : 'gathering';
  connectionState = 'new';
  localDescription: { sdp: string } | null = null;
  ontrack: ((event: { streams: MediaStream[] }) => void) | null = null;
  onconnectionstatechange: (() => void) | null = null;
  iceConnectionState = 'new';
  oniceconnectionstatechange: (() => void) | null = null;
  onicegatheringstatechange: (() => void) | null = null;
  constructor() { FakePeerConnection.instances.push(this); }
  createDataChannel = vi.fn(() => this.dc);
  sender = {
    track: null as MediaStreamTrack | null,
    replaceTrack: vi.fn(async (track: MediaStreamTrack | null) => { this.sender.track = track; }),
  };
  addTrack: Mock = vi.fn((track: MediaStreamTrack) => { this.sender.track = track; return this.sender; });
  addTransceiver = vi.fn(() => ({ sender: this.sender }));
  createOffer = vi.fn(async () => {
    if (this.dc.onmessage == null || this.ontrack == null || this.onicegatheringstatechange == null || this.dc.onopen == null) throw new Error('Listeners were installed too late');
    return { type: 'offer', sdp: 'local-offer' };
  });
  setLocalDescription = vi.fn(async (offer: { sdp: string }) => { this.localDescription = offer; });
  setRemoteDescription = vi.fn(async () => {
    if (FakePeerConnection.autoOpen) this.dc.open();
    if (FakePeerConnection.autoStart) this.dc.emit({ type: 'session-started', sessionId: 'session-1', delegationMode: 'client', raw: {} });
  });
  close = vi.fn(() => { this.connectionState = 'closed'; this.onconnectionstatechange?.(); });
}

type FakeAudio = {
  autoplay: boolean;
  srcObject: MediaStream | null;
  onplaying: (() => void) | null;
  onpause: (() => void) | null;
  onwaiting: (() => void) | null;
  onended: (() => void) | null;
  play: Mock<() => Promise<void>>;
  pause: Mock;
};

export function installWebRTC(): {
  track: FakeTrack;
  stream: MediaStream;
  getUserMedia: Mock<() => Promise<MediaStream>>;
  audio: FakeAudio;
  fetch: Mock<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>;
} {
  FakePeerConnection.instances = [];
  FakePeerConnection.autoOpen = true;
  FakePeerConnection.autoStart = true;
  FakePeerConnection.gather = true;
  const media = fakeStream();
  const getUserMedia = vi.fn(async () => media.stream);
  const audio = {
    autoplay: false,
    srcObject: null as MediaStream | null,
    onplaying: null as (() => void) | null,
    onpause: null as (() => void) | null,
    onwaiting: null as (() => void) | null,
    onended: null as (() => void) | null,
    play: vi.fn(async () => {}),
    pause: vi.fn(),
  };
  vi.stubGlobal('RTCPeerConnection', FakePeerConnection);
  vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
  // Preserve the DOM used by React tests while substituting its audio element.
  if (typeof document === 'undefined') {
    vi.stubGlobal('document', { createElement: vi.fn(() => audio) });
  } else {
    const createElement = document.createElement.bind(document);
    vi.spyOn(document, 'createElement').mockImplementation(((tag: string, options?: ElementCreationOptions) =>
      tag === 'audio' ? audio : createElement(tag, options)) as typeof document.createElement);
  }
  const fetch = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(async () => Response.json({ sessionId: 'session-1', sdp: 'remote-answer' }));
  vi.stubGlobal('fetch', fetch);
  return { ...media, getUserMedia, audio, fetch };
}

export function liveModel(): RealtimeModel & { capabilities: NonNullable<RealtimeModel['capabilities']> } {
  return {
    specificationVersion: 'v4', provider: 'test', modelId: 'live',
    capabilities: { conversation: 'continuous', transports: ['webrtc', 'websocket'], connections: ['webrtc', 'server-websocket'], startup: 'session-start', finalization: 'session-close' },
    getWebRTCConfig: () => ({ dataChannelLabel: 'model-events' }),
    getWebSocketConfig: vi.fn(),
    doCreateClientSecret: vi.fn(),
    buildSessionConfig: vi.fn(),
    parseServerEvent: event => event as RealtimeServerEvent,
    serializeClientEvent: async event => event,
  };
}

export async function flushEvents() {
  for (let i = 0; i < 100; i++) await Promise.resolve();
}

export function deferred<T>() {
  let resolve: (value: T) => void = () => {};
  const promise = new Promise<T>(r => { resolve = r; });
  return { promise, resolve };
}
