/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Page } from '@playwright/test';

/**
 * Install RTCPeerConnection monkey-patch on a page BEFORE navigating.
 * Tracks all created peer connections and their remote tracks so tests
 * can inspect WebRTC state via `page.evaluate()`.
 *
 * Call immediately after page creation, before any `goto()`.
 */
export async function installWebRTCTracking(page: Page): Promise<void> {
  await page.addInitScript(() => {
    const connections: RTCPeerConnection[] = [];
    const dataChannels: RTCDataChannel[] = [];
    const syntheticMediaResources: {
      audioCtx: AudioContext;
      source?: AudioScheduledSourceNode;
      drawIntervalId?: number;
    }[] = [];

    (window as any).__rtcConnections = connections;
    (window as any).__rtcDataChannels = dataChannels;
    (window as any).__rtcRemoteTracks = [] as { kind: string; id: string; readyState: string }[];
    (window as any).__rtcSyntheticMediaResources = syntheticMediaResources;

    const OriginalRTCPeerConnection = window.RTCPeerConnection;
    const trackDataChannel = (channel: RTCDataChannel) => {
      if (dataChannels.includes(channel)) {
        return;
      }

      dataChannels.push(channel);
    };

    (window as any).RTCPeerConnection = function(this: RTCPeerConnection, ...args: any[]) {
      const pc: RTCPeerConnection = new OriginalRTCPeerConnection(...args);
      const originalCreateDataChannel = pc.createDataChannel.bind(pc);

      connections.push(pc);

      pc.createDataChannel = ((label: string, options?: RTCDataChannelInit) => {
        const channel = originalCreateDataChannel(label, options);

        trackDataChannel(channel);
        return channel;
      }) as RTCPeerConnection['createDataChannel'];

      pc.addEventListener('connectionstatechange', () => {
        (window as any).__lastRtcState = pc.connectionState;
      });

      pc.addEventListener('datachannel', (event: RTCDataChannelEvent) => {
        trackDataChannel(event.channel);
      });

      pc.addEventListener('track', (event: RTCTrackEvent) => {
        (window as any).__rtcRemoteTracks.push({
          kind: event.track.kind,
          id: event.track.id,
          readyState: event.track.readyState
        });
      });

      return pc;
    } as any;

    (window as any).RTCPeerConnection.prototype = OriginalRTCPeerConnection.prototype;
    Object.setPrototypeOf((window as any).RTCPeerConnection, OriginalRTCPeerConnection);

    // Patch getDisplayMedia to return a synthetic screen share stream
    // (canvas-based video + 880Hz oscillator audio) so the browser
    // picker dialog is never shown.
    navigator.mediaDevices.getDisplayMedia = async (_constraints?: DisplayMediaStreamOptions) => {
      const canvas = document.createElement('canvas');

      canvas.width = 640;
      canvas.height = 480;

      const ctx = canvas.getContext('2d');

      if (!ctx) {
        throw new Error('Canvas 2D context unavailable');
      }

      let frameCount = 0;

      // Draw animated frames so video stats show increasing bytes
      const drawFrame = () => {
        frameCount++;
        ctx.fillStyle = `hsl(${frameCount % 360}, 70%, 50%)`;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = '#fff';
        ctx.font = '24px monospace';
        ctx.fillText(`Screen Share Frame ${frameCount}`, 40, 60);
      };

      drawFrame();
      const drawInterval = setInterval(drawFrame, 100);
      const videoStream = canvas.captureStream(10); // 10 fps
      const videoTrack = videoStream.getVideoTracks()[0];

      // Stop drawing when the track ends
      videoTrack.addEventListener('ended', () => clearInterval(drawInterval));

      // Create 880Hz oscillator for screen share audio (distinct from 440Hz voice)
      const audioCtx = new AudioContext();
      const osc = audioCtx.createOscillator();

      osc.frequency.value = 880;

      const dest = audioCtx.createMediaStreamDestination();

      osc.connect(dest);
      osc.start();

      if (audioCtx.state === 'suspended') {
        try {
          await audioCtx.resume();
        } catch {}
      }

      const audioTrack = dest.stream.getAudioTracks()[0];
      // Combine video + audio into one stream
      const resultStream = new MediaStream([videoTrack, audioTrack]);

      syntheticMediaResources.push({
        audioCtx,
        source: osc,
        drawIntervalId: drawInterval as unknown as number
      });

      audioTrack.addEventListener('ended', () => {
        clearInterval(drawInterval);

        try {
          osc.stop();
        } catch {}

        void audioCtx.close().catch(() => {});
      }, { once: true });

      // Tag the stream so tests can identify it
      (resultStream as any).__isScreenShare = true;

      return resultStream;
    };
  });
}

/**
 * Wait until at least one RTCPeerConnection reaches the 'connected' state.
 */

/**
 * Ensure every `AudioContext` created by the page auto-resumes so that
 * the input-gain Web Audio pipeline (`source -> gain -> destination`) never
 * stalls in the "suspended" state.
 *
 * On Linux with multiple headless Chromium instances, `new AudioContext()`
 * can start suspended without a user-gesture gate, causing the media
 * pipeline to emit only a single RTP packet.
 *
 * Call once per page, BEFORE navigating, alongside `installWebRTCTracking`.
 */
export async function installAutoResumeAudioContext(page: Page): Promise<void> {
  await page.addInitScript(() => {
    const OrigAudioContext = window.AudioContext;

    (window as any).AudioContext = function(this: AudioContext, ...args: any[]) {
      const ctx: AudioContext = new OrigAudioContext(...args);
      // Track all created AudioContexts for test diagnostics
      const tracked = ((window as any).__trackedAudioContexts ??= []) as AudioContext[];

      tracked.push(ctx);

      if (ctx.state === 'suspended') {
        ctx.resume().catch(() => { /* noop */ });
      }

      // Also catch transitions to suspended after creation
      ctx.addEventListener('statechange', () => {
        if (ctx.state === 'suspended') {
          ctx.resume().catch(() => { /* noop */ });
        }
      });

      return ctx;
    } as any;

    (window as any).AudioContext.prototype = OrigAudioContext.prototype;
    Object.setPrototypeOf((window as any).AudioContext, OrigAudioContext);
  });
}

export async function waitForPeerConnected(page: Page, timeout = 30_000): Promise<void> {
  await page.waitForFunction(
    () => (window as any).__rtcConnections?.some(
      (pc: RTCPeerConnection) => pc.connectionState === 'connected'
    ) ?? false,
    { timeout }
  );
}

/**
 * Check that a peer connection is still in 'connected' state (not failed/disconnected).
 */
export async function isPeerStillConnected(page: Page): Promise<boolean> {
  return page.evaluate(
    () => (window as any).__rtcConnections?.some(
      (pc: RTCPeerConnection) => pc.connectionState === 'connected'
    ) ?? false
  );
}

/** Returns the number of tracked peer connections in `connected` state. */
export async function getConnectedPeerCount(page: Page): Promise<number> {
  return page.evaluate(
    () => ((window as any).__rtcConnections as RTCPeerConnection[] | undefined)?.filter(
      (pc) => pc.connectionState === 'connected'
    ).length ?? 0
  );
}

/** Wait until the expected number of peer connections are `connected`. */
export async function waitForConnectedPeerCount(page: Page, expectedCount: number, timeout = 45_000): Promise<void> {
  await page.waitForFunction(
    (count) => ((window as any).__rtcConnections as RTCPeerConnection[] | undefined)?.filter(
      (pc) => pc.connectionState === 'connected'
    ).length === count,
    expectedCount,
    { timeout }
  );
}

/** Returns the number of tracked RTCDataChannels in the open state. */
export async function getOpenDataChannelCount(page: Page): Promise<number> {
  return page.evaluate(
    () => ((window as any).__rtcDataChannels as RTCDataChannel[] | undefined)?.filter(
      (channel) => channel.readyState === 'open'
    ).length ?? 0
  );
}

/** Wait until the expected number of tracked RTCDataChannels are open. */
export async function waitForOpenDataChannelCount(page: Page, expectedCount: number, timeout = 45_000): Promise<void> {
  await page.waitForFunction(
    (count) => ((window as any).__rtcDataChannels as RTCDataChannel[] | undefined)?.filter(
      (channel) => channel.readyState === 'open'
    ).length === count,
    expectedCount,
    { timeout }
  );
}

/** Close every currently-open RTCDataChannel and return how many were closed. */
export async function closeOpenDataChannels(page: Page): Promise<number> {
  return page.evaluate(() => {
    const channels = ((window as any).__rtcDataChannels as RTCDataChannel[] | undefined) ?? [];

    let closed = 0;

    for (const channel of channels) {
      if (channel.readyState !== 'open') {
        continue;
      }

      channel.close();
      closed++;
    }

    return closed;
  });
}

/** Dispatch a synthetic data-channel error event on each open channel. */
export async function dispatchDataChannelErrors(page: Page): Promise<number> {
  return page.evaluate(() => {
    const channels = ((window as any).__rtcDataChannels as RTCDataChannel[] | undefined) ?? [];

    let dispatched = 0;

    for (const channel of channels) {
      if (channel.readyState !== 'open') {
        continue;
      }

      channel.dispatchEvent(new Event('error'));
      dispatched++;
    }

    return dispatched;
  });
}

/**
 * Resume all suspended AudioContext instances created by the synthetic
 * media patch.  Uses CDP `Runtime.evaluate` with `userGesture: true` so
 * Chrome treats the call as a user-gesture - this satisfies the autoplay
 * policy that otherwise blocks `AudioContext.resume()`.
 */
export async function resumeSyntheticAudioContexts(page: Page): Promise<number> {
  const cdpSession = await page.context().newCDPSession(page);

  try {
    const result = await cdpSession.send('Runtime.evaluate', {
      expression: `(async () => {
        const resources = window.__rtcSyntheticMediaResources;
        if (!resources) return 0;
        let resumed = 0;
        for (const r of resources) {
          if (r.audioCtx.state === 'suspended') {
            await r.audioCtx.resume();
            resumed++;
          }
        }
        return resumed;
      })()`,
      awaitPromise: true,
      userGesture: true
    });

    return result.result.value ?? 0;
  } finally {
    await cdpSession.detach();
  }
}

interface PerPeerAudioStat {
  connectionState: string;
  inboundBytes: number;
  inboundPackets: number;
  outboundBytes: number;
  outboundPackets: number;
}

/** Get per-peer audio stats for every tracked RTCPeerConnection. */
export async function getPerPeerAudioStats(page: Page): Promise<PerPeerAudioStat[]> {
  return page.evaluate(async () => {
    const connections = (window as any).__rtcConnections as RTCPeerConnection[] | undefined;

    if (!connections?.length) {
      return [];
    }

    const snapshots: PerPeerAudioStat[] = [];

    for (const pc of connections) {
      let inboundBytes = 0;
      let inboundPackets = 0;
      let outboundBytes = 0;
      let outboundPackets = 0;

      try {
        const stats = await pc.getStats();

        stats.forEach((report: any) => {
          const kind = report.kind ?? report.mediaType;

          if (report.type === 'outbound-rtp' && kind === 'audio') {
            outboundBytes += report.bytesSent ?? 0;
            outboundPackets += report.packetsSent ?? 0;
          }

          if (report.type === 'inbound-rtp' && kind === 'audio') {
            inboundBytes += report.bytesReceived ?? 0;
            inboundPackets += report.packetsReceived ?? 0;
          }
        });
      } catch {
        // Closed connection.
      }

      snapshots.push({
        connectionState: pc.connectionState,
        inboundBytes,
        inboundPackets,
        outboundBytes,
        outboundPackets
      });
    }

    return snapshots;
  });
}

/** Wait until every connected peer connection shows inbound and outbound audio flow. */
export async function waitForAllPeerAudioFlow(
  page: Page,
  expectedConnectedPeers: number,
  timeoutMs = 45_000,
  pollIntervalMs = 1_000
): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  // Track which peer indices have been confirmed flowing at least once.
  // This prevents a peer from being missed just because it briefly paused
  // during one specific poll interval.
  const confirmedFlowing = new Set<number>();

  let previous = await getPerPeerAudioStats(page);

  while (Date.now() < deadline) {
    await page.waitForTimeout(pollIntervalMs);
    const current = await getPerPeerAudioStats(page);
    const connectedPeers = current.filter((stat) => stat.connectionState === 'connected');

    if (connectedPeers.length >= expectedConnectedPeers) {
      for (let index = 0; index < current.length; index++) {
        const curr = current[index];

        if (!curr || curr.connectionState !== 'connected') {
          continue;
        }

        const prev = previous[index] ?? {
          connectionState: 'new',
          inboundBytes: 0,
          inboundPackets: 0,
          outboundBytes: 0,
          outboundPackets: 0
        };
        const inboundFlowing = curr.inboundBytes > prev.inboundBytes || curr.inboundPackets > prev.inboundPackets;
        const outboundFlowing = curr.outboundBytes > prev.outboundBytes || curr.outboundPackets > prev.outboundPackets;

        if (inboundFlowing && outboundFlowing) {
          confirmedFlowing.add(index);
        }
      }

      // Check if enough peers have been confirmed across all samples
      const connectedIndices = current
        .map((stat, idx) => stat.connectionState === 'connected' ? idx : -1)
        .filter((idx) => idx >= 0);
      const confirmedCount = connectedIndices.filter((idx) => confirmedFlowing.has(idx)).length;

      if (confirmedCount >= expectedConnectedPeers) {
        return;
      }
    }

    previous = current;
  }

  throw new Error(`Timed out waiting for ${expectedConnectedPeers} peers with bidirectional audio flow`);
}

/**
 * Get outbound and inbound audio RTP stats aggregated across all peer
 * connections.  Uses a per-connection high water mark stored on `window` so
 * that connections that close mid-measurement still contribute their last
 * known counters, preventing the aggregate from going backwards.
 */
export async function getAudioStats(page: Page): Promise<{
  outbound: { bytesSent: number; packetsSent: number } | null;
  inbound: { bytesReceived: number; packetsReceived: number } | null;
}> {
  return page.evaluate(async () => {
    const connections = (window as any).__rtcConnections as RTCPeerConnection[] | undefined;

    if (!connections?.length)
      return { outbound: null, inbound: null };

    interface HWMEntry {
      outBytesSent: number;
      outPacketsSent: number;
      inBytesReceived: number;
      inPacketsReceived: number;
      hasOutbound: boolean;
      hasInbound: boolean;
    };

    const hwm: Record<number, HWMEntry> = (window as any).__rtcStatsHWM =
      ((window as any).__rtcStatsHWM as Record<number, HWMEntry> | undefined) ?? {};

    for (let idx = 0; idx < connections.length; idx++) {
      let stats: RTCStatsReport;

      try {
        stats = await connections[idx].getStats();
      } catch {
        continue; // closed connection - keep its last HWM
      }

      let obytes = 0;
      let opackets = 0;
      let ibytes = 0;
      let ipackets = 0;
      let hasOut = false;
      let hasIn = false;

      stats.forEach((report: any) => {
        const kind = report.kind ?? report.mediaType;

        if (report.type === 'outbound-rtp' && kind === 'audio') {
          hasOut = true;
          obytes += report.bytesSent ?? 0;
          opackets += report.packetsSent ?? 0;
        }

        if (report.type === 'inbound-rtp' && kind === 'audio') {
          hasIn = true;
          ibytes += report.bytesReceived ?? 0;
          ipackets += report.packetsReceived ?? 0;
        }
      });

      if (hasOut || hasIn) {
        hwm[idx] = {
          outBytesSent: obytes,
          outPacketsSent: opackets,
          inBytesReceived: ibytes,
          inPacketsReceived: ipackets,
          hasOutbound: hasOut,
          hasInbound: hasIn
        };
      }
    }

    let totalOutBytes = 0;
    let totalOutPackets = 0;
    let totalInBytes = 0;
    let totalInPackets = 0;
    let anyOutbound = false;
    let anyInbound = false;

    for (const entry of Object.values(hwm)) {
      totalOutBytes += entry.outBytesSent;
      totalOutPackets += entry.outPacketsSent;
      totalInBytes += entry.inBytesReceived;
      totalInPackets += entry.inPacketsReceived;

      if (entry.hasOutbound)
        anyOutbound = true;

      if (entry.hasInbound)
        anyInbound = true;
    }

    return {
      outbound: anyOutbound
        ? { bytesSent: totalOutBytes, packetsSent: totalOutPackets }
        : null,
      inbound: anyInbound
        ? { bytesReceived: totalInBytes, packetsReceived: totalInPackets }
        : null
    };
  });
}

/**
 * Snapshot audio stats, wait `durationMs`, snapshot again, and return the delta.
 * Useful for verifying audio is actively flowing (bytes increasing).
 */
export async function getAudioStatsDelta(page: Page, durationMs = 3_000): Promise<{
  outboundBytesDelta: number;
  inboundBytesDelta: number;
  outboundPacketsDelta: number;
  inboundPacketsDelta: number;
}> {
  const before = await getAudioStats(page);

  await page.waitForTimeout(durationMs);

  const after = await getAudioStats(page);

  return {
    outboundBytesDelta: (after.outbound?.bytesSent ?? 0) - (before.outbound?.bytesSent ?? 0),
    inboundBytesDelta: (after.inbound?.bytesReceived ?? 0) - (before.inbound?.bytesReceived ?? 0),
    outboundPacketsDelta: (after.outbound?.packetsSent ?? 0) - (before.outbound?.packetsSent ?? 0),
    inboundPacketsDelta: (after.inbound?.packetsReceived ?? 0) - (before.inbound?.packetsReceived ?? 0)
  };
}

/**
 * Wait until at least one connection has both outbound-rtp and inbound-rtp
 * audio reports.  Call after `waitForPeerConnected` to ensure the audio
 * pipeline is ready before measuring deltas.
 */
export async function waitForAudioStatsPresent(page: Page, timeout = 15_000): Promise<void> {
  await page.waitForFunction(
    async () => {
      const connections = (window as any).__rtcConnections as RTCPeerConnection[] | undefined;

      if (!connections?.length)
        return false;

      for (const pc of connections) {
        let stats: RTCStatsReport;

        try {
          stats = await pc.getStats();
        } catch {
          continue;
        }

        let hasOut = false;
        let hasIn = false;

        stats.forEach((report: any) => {
          const kind = report.kind ?? report.mediaType;

          if (report.type === 'outbound-rtp' && kind === 'audio')
            hasOut = true;

          if (report.type === 'inbound-rtp' && kind === 'audio')
            hasIn = true;
        });

        if (hasOut && hasIn)
          return true;
      }

      return false;
    },
    { timeout }
  );
}

interface AudioFlowDelta {
  outboundBytesDelta: number;
  inboundBytesDelta: number;
  outboundPacketsDelta: number;
  inboundPacketsDelta: number;
}

function snapshotToDelta(
  curr: Awaited<ReturnType<typeof getAudioStats>>,
  prev: Awaited<ReturnType<typeof getAudioStats>>
): AudioFlowDelta {
  return {
    outboundBytesDelta: (curr.outbound?.bytesSent ?? 0) - (prev.outbound?.bytesSent ?? 0),
    inboundBytesDelta: (curr.inbound?.bytesReceived ?? 0) - (prev.inbound?.bytesReceived ?? 0),
    outboundPacketsDelta: (curr.outbound?.packetsSent ?? 0) - (prev.outbound?.packetsSent ?? 0),
    inboundPacketsDelta: (curr.inbound?.packetsReceived ?? 0) - (prev.inbound?.packetsReceived ?? 0)
  };
}

function isDeltaFlowing(delta: AudioFlowDelta): boolean {
  const outFlowing = delta.outboundBytesDelta > 0 || delta.outboundPacketsDelta > 0;
  const inFlowing = delta.inboundBytesDelta > 0 || delta.inboundPacketsDelta > 0;

  return outFlowing && inFlowing;
}

/**
 * Poll until two consecutive HWM-based reads show both outbound and inbound
 * audio byte counts increasing.  Combines per-connection high-water marks
 * (which prevent totals from going backwards after connection churn) with
 * consecutive comparison (which avoids a stale single baseline).
 */
export async function waitForAudioFlow(
  page: Page,
  timeoutMs = 30_000,
  pollIntervalMs = 1_000
): Promise<AudioFlowDelta> {
  const deadline = Date.now() + timeoutMs;

  let prev = await getAudioStats(page);

  while (Date.now() < deadline) {
    await page.waitForTimeout(pollIntervalMs);
    const curr = await getAudioStats(page);
    const delta = snapshotToDelta(curr, prev);

    if (isDeltaFlowing(delta)) {
      return delta;
    }

    prev = curr;
  }

  // Timeout - return zero deltas so the caller's assertion reports the failure.
  return {
    outboundBytesDelta: 0,
    inboundBytesDelta: 0,
    outboundPacketsDelta: 0,
    inboundPacketsDelta: 0
  };
}

/**
 * Get outbound and inbound video RTP stats aggregated across all peer
 * connections.  Uses the same HWM pattern as {@link getAudioStats}.
 */
export async function getVideoStats(page: Page): Promise<{
  outbound: { bytesSent: number; packetsSent: number } | null;
  inbound: { bytesReceived: number; packetsReceived: number } | null;
}> {
  return page.evaluate(async () => {
    const connections = (window as any).__rtcConnections as RTCPeerConnection[] | undefined;

    if (!connections?.length)
      return { outbound: null, inbound: null };

    interface VHWM {
      outBytesSent: number;
      outPacketsSent: number;
      inBytesReceived: number;
      inPacketsReceived: number;
      hasOutbound: boolean;
      hasInbound: boolean;
    }

    const hwm: Record<number, VHWM> = (window as any).__rtcVideoStatsHWM =
      ((window as any).__rtcVideoStatsHWM as Record<number, VHWM> | undefined) ?? {};

    for (let idx = 0; idx < connections.length; idx++) {
      let stats: RTCStatsReport;

      try {
        stats = await connections[idx].getStats();
      } catch {
        continue;
      }

      let obytes = 0;
      let opackets = 0;
      let ibytes = 0;
      let ipackets = 0;
      let hasOut = false;
      let hasIn = false;

      stats.forEach((report: any) => {
        const kind = report.kind ?? report.mediaType;

        if (report.type === 'outbound-rtp' && kind === 'video') {
          hasOut = true;
          obytes += report.bytesSent ?? 0;
          opackets += report.packetsSent ?? 0;
        }

        if (report.type === 'inbound-rtp' && kind === 'video') {
          hasIn = true;
          ibytes += report.bytesReceived ?? 0;
          ipackets += report.packetsReceived ?? 0;
        }
      });

      if (hasOut || hasIn) {
        hwm[idx] = {
          outBytesSent: obytes,
          outPacketsSent: opackets,
          inBytesReceived: ibytes,
          inPacketsReceived: ipackets,
          hasOutbound: hasOut,
          hasInbound: hasIn
        };
      }
    }

    let totalOutBytes = 0;
    let totalOutPackets = 0;
    let totalInBytes = 0;
    let totalInPackets = 0;
    let anyOutbound = false;
    let anyInbound = false;

    for (const entry of Object.values(hwm)) {
      totalOutBytes += entry.outBytesSent;
      totalOutPackets += entry.outPacketsSent;
      totalInBytes += entry.inBytesReceived;
      totalInPackets += entry.inPacketsReceived;

      if (entry.hasOutbound)
        anyOutbound = true;

      if (entry.hasInbound)
        anyInbound = true;
    }

    return {
      outbound: anyOutbound
        ? { bytesSent: totalOutBytes, packetsSent: totalOutPackets }
        : null,
      inbound: anyInbound
        ? { bytesReceived: totalInBytes, packetsReceived: totalInPackets }
        : null
    };
  });
}

/**
 * Wait until at least one connection has both outbound-rtp and inbound-rtp
 * video reports.
 */
export async function waitForVideoStatsPresent(page: Page, timeout = 15_000): Promise<void> {
  await page.waitForFunction(
    async () => {
      const connections = (window as any).__rtcConnections as RTCPeerConnection[] | undefined;

      if (!connections?.length)
        return false;

      for (const pc of connections) {
        let stats: RTCStatsReport;

        try {
          stats = await pc.getStats();
        } catch {
          continue;
        }

        let hasOut = false;
        let hasIn = false;

        stats.forEach((report: any) => {
          const kind = report.kind ?? report.mediaType;

          if (report.type === 'outbound-rtp' && kind === 'video')
            hasOut = true;

          if (report.type === 'inbound-rtp' && kind === 'video')
            hasIn = true;
        });

        if (hasOut && hasIn)
          return true;
      }

      return false;
    },
    { timeout }
  );
}

interface VideoFlowDelta {
  outboundBytesDelta: number;
  inboundBytesDelta: number;
  outboundPacketsDelta: number;
  inboundPacketsDelta: number;
}

function videoSnapshotToDelta(
  curr: Awaited<ReturnType<typeof getVideoStats>>,
  prev: Awaited<ReturnType<typeof getVideoStats>>
): VideoFlowDelta {
  return {
    outboundBytesDelta: (curr.outbound?.bytesSent ?? 0) - (prev.outbound?.bytesSent ?? 0),
    inboundBytesDelta: (curr.inbound?.bytesReceived ?? 0) - (prev.inbound?.bytesReceived ?? 0),
    outboundPacketsDelta: (curr.outbound?.packetsSent ?? 0) - (prev.outbound?.packetsSent ?? 0),
    inboundPacketsDelta: (curr.inbound?.packetsReceived ?? 0) - (prev.inbound?.packetsReceived ?? 0)
  };
}

function isVideoDeltaFlowing(delta: VideoFlowDelta): boolean {
  const outFlowing = delta.outboundBytesDelta > 0 || delta.outboundPacketsDelta > 0;
  const inFlowing = delta.inboundBytesDelta > 0 || delta.inboundPacketsDelta > 0;

  return outFlowing && inFlowing;
}

/**
 * Poll until two consecutive HWM-based reads show both outbound and inbound
 * video byte counts increasing - proving screen share video is flowing.
 */
export async function waitForVideoFlow(
  page: Page,
  timeoutMs = 30_000,
  pollIntervalMs = 1_000
): Promise<VideoFlowDelta> {
  const deadline = Date.now() + timeoutMs;

  let prev = await getVideoStats(page);

  while (Date.now() < deadline) {
    await page.waitForTimeout(pollIntervalMs);
    const curr = await getVideoStats(page);
    const delta = videoSnapshotToDelta(curr, prev);

    if (isVideoDeltaFlowing(delta)) {
      return delta;
    }

    prev = curr;
  }

  return {
    outboundBytesDelta: 0,
    inboundBytesDelta: 0,
    outboundPacketsDelta: 0,
    inboundPacketsDelta: 0
  };
}

/**
 * Wait until outbound video bytes are increasing (sender side).
 * Use on the page that is sharing its screen.
 */
export async function waitForOutboundVideoFlow(
  page: Page,
  timeoutMs = 30_000,
  pollIntervalMs = 1_000
): Promise<VideoFlowDelta> {
  const deadline = Date.now() + timeoutMs;

  let prev = await getVideoStats(page);

  while (Date.now() < deadline) {
    await page.waitForTimeout(pollIntervalMs);
    const curr = await getVideoStats(page);
    const delta = videoSnapshotToDelta(curr, prev);

    if (delta.outboundBytesDelta > 0 || delta.outboundPacketsDelta > 0) {
      return delta;
    }

    prev = curr;
  }

  return {
    outboundBytesDelta: 0,
    inboundBytesDelta: 0,
    outboundPacketsDelta: 0,
    inboundPacketsDelta: 0
  };
}

/**
 * Wait until inbound video bytes are increasing (receiver side).
 * Use on the page that is viewing someone else's screen share.
 */
export async function waitForInboundVideoFlow(
  page: Page,
  timeoutMs = 30_000,
  pollIntervalMs = 1_000
): Promise<VideoFlowDelta> {
  const deadline = Date.now() + timeoutMs;

  let prev = await getVideoStats(page);

  while (Date.now() < deadline) {
    await page.waitForTimeout(pollIntervalMs);
    const curr = await getVideoStats(page);
    const delta = videoSnapshotToDelta(curr, prev);

    if (delta.inboundBytesDelta > 0 || delta.inboundPacketsDelta > 0) {
      return delta;
    }

    prev = curr;
  }

  return {
    outboundBytesDelta: 0,
    inboundBytesDelta: 0,
    outboundPacketsDelta: 0,
    inboundPacketsDelta: 0
  };
}

/**
 * Dump full RTC connection diagnostics for debugging audio flow failures.
 */
export async function dumpRtcDiagnostics(page: Page): Promise<string> {
  return page.evaluate(async () => {
    const conns = (window as any).__rtcConnections as RTCPeerConnection[] | undefined;

    if (!conns?.length)
      return 'No connections tracked';

    const lines: string[] = [`Total connections: ${conns.length}`];

    for (let idx = 0; idx < conns.length; idx++) {
      const pc = conns[idx];

      lines.push(`PC[${idx}]: connection=${pc.connectionState}, signaling=${pc.signalingState}`);

      const senders = pc.getSenders().map(
        (sender) => `${sender.track?.kind ?? 'none'}:enabled=${sender.track?.enabled}:${sender.track?.readyState ?? 'null'}`
      );
      const receivers = pc.getReceivers().map(
        (recv) => `${recv.track?.kind ?? 'none'}:enabled=${recv.track?.enabled}:${recv.track?.readyState ?? 'null'}`
      );

      lines.push(`  senders=[${senders.join(', ')}]`);
      lines.push(`  receivers=[${receivers.join(', ')}]`);

      try {
        const stats = await pc.getStats();

        stats.forEach((report: any) => {
          if (report.type !== 'outbound-rtp' && report.type !== 'inbound-rtp')
            return;

          const kind = report.kind ?? report.mediaType;
          const bytes = report.type === 'outbound-rtp' ? report.bytesSent : report.bytesReceived;
          const packets = report.type === 'outbound-rtp' ? report.packetsSent : report.packetsReceived;

          lines.push(`  ${report.type}: kind=${kind}, bytes=${bytes}, packets=${packets}`);
        });
      } catch (err: any) {
        lines.push(`  getStats() failed: ${err?.message ?? err}`);
      }
    }

    return lines.join('\n');
  });
}
