import { expect, type Page } from '@playwright/test';
import { test, type Client } from '../../fixtures/multi-client';
import {
  closeOpenDataChannels,
  dispatchDataChannelErrors,
  dumpRtcDiagnostics,
  getOpenDataChannelCount,
  installAutoResumeAudioContext,
  installWebRTCTracking,
  waitForAllPeerAudioFlow,
  waitForAudioStatsPresent,
  waitForConnectedPeerCount,
  waitForOpenDataChannelCount
} from '../../helpers/webrtc-helpers';
import { RegisterPage } from '../../pages/register.page';
import { ServerSearchPage } from '../../pages/server-search.page';
import { ChatRoomPage } from '../../pages/chat-room.page';

interface VoiceClient extends Client {
  displayName: string;
  username: string;
}

const USER_PASSWORD = 'TestPass123!';
const VOICE_CHANNEL = 'General';

test.describe('Voice data-channel recovery', () => {
  test('keeps two users hearing each other after a data-channel error and close', async ({ createClient }) => {
    test.setTimeout(240_000);

    const clients = await createVoiceScenario(createClient, 2, `DC Recovery Duo ${Date.now()}`);
    const [alice, bob] = clients;

    await assertMeshAudio(clients, 1, 'initial two-user voice');

    await test.step('A non-fatal data-channel error does not interrupt audio', async () => {
      const dispatched = await dispatchDataChannelErrors(alice.page);

      expect(dispatched).toBeGreaterThan(0);
      await waitForOpenDataChannelCount(alice.page, 1, 15_000);
      await waitForOpenDataChannelCount(bob.page, 1, 15_000);
      await assertMeshAudio(clients, 1, 'after synthetic data-channel error');
    });

    await test.step('A closed data channel is rebuilt and audio resumes both ways', async () => {
      const closed = await closeOpenDataChannels(alice.page);

      expect(closed).toBeGreaterThan(0);
      await waitForConnectedPeerCount(alice.page, 1, 60_000);
      await waitForConnectedPeerCount(bob.page, 1, 60_000);
      await waitForOpenDataChannelCount(alice.page, 1, 60_000);
      await waitForOpenDataChannelCount(bob.page, 1, 60_000);
      await assertMeshAudio(clients, 1, 'after data-channel close recovery');
    });
  });

  test('heals a three-user voice mesh when one client loses every data channel', async ({ createClient }) => {
    test.setTimeout(300_000);

    const clients = await createVoiceScenario(createClient, 3, `DC Recovery Trio ${Date.now()}`);
    const bob = clients[1];

    await assertMeshAudio(clients, 2, 'initial three-user mesh');

    await test.step('Bob loses all control channels and the full mesh recovers', async () => {
      const closed = await closeOpenDataChannels(bob.page);

      expect(closed).toBe(2);

      for (const client of clients) {
        await waitForConnectedPeerCount(client.page, 2, 90_000);
        await waitForOpenDataChannelCount(client.page, 2, 90_000);
      }

      await assertMeshAudio(clients, 2, 'after full control-channel recovery');
    });
  });
});

async function createVoiceScenario(
  createClient: () => Promise<Client>,
  userCount: number,
  serverName: string
): Promise<VoiceClient[]> {
  const clients: VoiceClient[] = [];

  for (let index = 0; index < userCount; index++) {
    const client = await createClient();
    const displayName = `DC Voice ${index + 1}`;

    await installDeterministicVoiceSettings(client.page);
    await installWebRTCTracking(client.page);
    await installAutoResumeAudioContext(client.page);

    clients.push({
      ...client,
      displayName,
      username: `dc_voice_${Date.now()}_${index + 1}`
    });
  }

  await test.step('Register clients', async () => {
    for (const client of clients) {
      const registerPage = new RegisterPage(client.page);

      await registerPage.goto();
      await registerPage.register(client.username, client.displayName, USER_PASSWORD);
      await expect(client.page).toHaveURL(/\/search/, { timeout: 20_000 });
    }
  });

  await test.step('Create and join server', async () => {
    const hostSearch = new ServerSearchPage(clients[0].page);

    await hostSearch.createServer(serverName, { description: 'Data-channel recovery voice test' });
    await expect(clients[0].page).toHaveURL(/\/room\//, { timeout: 20_000 });

    for (const client of clients.slice(1)) {
      const searchPage = new ServerSearchPage(client.page);

      await searchPage.joinServerFromSearch(serverName);
      await expect(client.page).toHaveURL(/\/room\//, { timeout: 20_000 });
    }
  });

  await test.step('Join everyone to voice', async () => {
    const hostRoom = new ChatRoomPage(clients[0].page);

    await hostRoom.ensureVoiceChannelExists(VOICE_CHANNEL);

    for (const client of clients) {
      const room = new ChatRoomPage(client.page);

      await room.joinVoiceChannel(VOICE_CHANNEL);
      await expect(room.voiceControls).toBeVisible({ timeout: 20_000 });
    }

    const expectedRemotePeers = clients.length - 1;

    for (const client of clients) {
      await waitForConnectedPeerCount(client.page, expectedRemotePeers, 90_000);
      await waitForOpenDataChannelCount(client.page, expectedRemotePeers, 90_000);
      await waitForAudioStatsPresent(client.page, 30_000);
    }
  });

  return clients;
}

async function installDeterministicVoiceSettings(page: Page): Promise<void> {
  await page.addInitScript(() => {
    localStorage.setItem('metoyou_voice_settings', JSON.stringify({
      inputVolume: 100,
      outputVolume: 100,
      audioBitrate: 96,
      latencyProfile: 'balanced',
      includeSystemAudio: false,
      noiseReduction: false,
      screenShareQuality: 'balanced',
      askScreenShareQuality: false
    }));
  });
}

async function assertMeshAudio(
  clients: readonly VoiceClient[],
  expectedRemotePeers: number,
  label: string
): Promise<void> {
  for (const client of clients) {
    try {
      await waitForAllPeerAudioFlow(client.page, expectedRemotePeers, 60_000);
    } catch (error) {
      const dataChannelCount = await getOpenDataChannelCount(client.page);

      console.log(`[${client.displayName} ${label} data channels] ${dataChannelCount}`);
      console.log(`[${client.displayName} ${label} RTC]\n${await dumpRtcDiagnostics(client.page)}`);
      throw error;
    }
  }
}
