import { test, expect } from '../../fixtures/multi-client';
import { expectDashboardReady } from '../../helpers/dashboard';
import { RegisterPage } from '../../pages/register.page';
import { ServerSearchPage } from '../../pages/server-search.page';
import { ChatRoomPage } from '../../pages/chat-room.page';
import {
  dumpRtcDiagnostics,
  installAutoResumeAudioContext,
  installWebRTCTracking,
  waitForAllPeerAudioFlow,
  waitForPeerConnected,
  waitForConnectedPeerCount,
  waitForAudioStatsPresent
} from '../../helpers/webrtc-helpers';

const ICE_STORAGE_KEY = 'metoyou_ice_servers';

interface StoredIceServerEntry {
  type?: string;
  urls?: string;
}

/**
 * Tests that user-configured ICE servers are persisted and used by peer connections.
 *
 * On localhost TURN relay is never needed (direct always succeeds), so this test:
 * 1. Seeds Bob's browser with an additional TURN entry via localStorage.
 * 2. Has both users join voice with differing ICE configs.
 * 3. Verifies both can connect and Bob's TURN entry is still in storage.
 */
test.describe('STUN/TURN fallback behaviour', () => {
  test.describe.configure({ timeout: 180_000 });

  test('users with different ICE configs can voice chat together', async ({ createClient }) => {
    const suffix = `turnfb_${Date.now()}`;
    const serverName = `Fallback ${suffix}`;
    const alice = await createClient();
    const bob = await createClient();

    // Install WebRTC tracking before any navigation so we can inspect
    // peer connections and audio stats.
    await installWebRTCTracking(alice.page);
    await installWebRTCTracking(bob.page);

    // Ensure AudioContexts auto-resume so the input-gain pipeline
    // (source -> gain -> destination) never stalls in "suspended" state.
    await installAutoResumeAudioContext(alice.page);
    await installAutoResumeAudioContext(bob.page);

    // Set deterministic voice settings so noise reduction and input gating
    // don't swallow the fake audio tone.
    const voiceSettings = JSON.stringify({
      inputVolume: 100,
      outputVolume: 100,
      audioBitrate: 96,
      latencyProfile: 'balanced',
      includeSystemAudio: false,
      noiseReduction: false,
      screenShareQuality: 'balanced',
      askScreenShareQuality: false
    });

    await alice.page.addInitScript((settings: string) => {
      localStorage.setItem('metoyou_voice_settings', settings);
    }, voiceSettings);

    await bob.page.addInitScript((settings: string) => {
      localStorage.setItem('metoyou_voice_settings', settings);
    }, voiceSettings);

    // Seed Bob with an extra TURN entry before the app reads localStorage.
    await bob.context.addInitScript((key: string) => {
      try {
        const existing = JSON.parse(localStorage.getItem(key) || '[]');

        existing.push({
          id: 'e2e-turn',
          type: 'turn',
          urls: 'turn:localhost:3478',
          username: 'e2euser',
          credential: 'e2epass'
        });

        localStorage.setItem(key, JSON.stringify(existing));
      } catch { /* noop */ }
    }, ICE_STORAGE_KEY);

    await test.step('Register Alice', async () => {
      const register = new RegisterPage(alice.page);

      await register.goto();
      await register.register(`alice_${suffix}`, 'Alice', 'TestPass123!');
      await expectDashboardReady(alice.page);
    });

    await test.step('Register Bob', async () => {
      const register = new RegisterPage(bob.page);

      await register.goto();
      await register.register(`bob_${suffix}`, 'Bob', 'TestPass123!');
      await expectDashboardReady(bob.page);
    });

    await test.step('Alice creates a server', async () => {
      const search = new ServerSearchPage(alice.page);

      await search.createServer(serverName);
    });

    await test.step('Bob joins Alice server', async () => {
      const search = new ServerSearchPage(bob.page);

      await search.joinServerFromSearch(serverName);
      await expect(bob.page).toHaveURL(/\/room\//, { timeout: 15_000 });
    });

    const aliceRoom = new ChatRoomPage(alice.page);
    const bobRoom = new ChatRoomPage(bob.page);

    await test.step('Both join voice', async () => {
      await aliceRoom.joinVoiceChannel('General');
      await bobRoom.joinVoiceChannel('General');
    });

    await test.step('Both users see each other in voice', async () => {
      await expect(
        aliceRoom.channelsSidePanel.getByText('Bob')
      ).toBeVisible({ timeout: 20_000 });

      await expect(
        bobRoom.channelsSidePanel.getByText('Alice')
      ).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Peer connections establish and audio flows bidirectionally', async () => {
      await waitForPeerConnected(alice.page, 30_000);
      await waitForPeerConnected(bob.page, 30_000);
      await waitForConnectedPeerCount(alice.page, 1, 30_000);
      await waitForConnectedPeerCount(bob.page, 1, 30_000);

      // Wait for audio RTP stats to appear (tracks negotiated)
      await waitForAudioStatsPresent(alice.page, 30_000);
      await waitForAudioStatsPresent(bob.page, 30_000);

      // Allow mesh to settle - voice routing and renegotiation can
      // cause a second offer/answer cycle after the initial connection.
      await alice.page.waitForTimeout(5_000);

      // Chromium's --use-fake-device-for-media-stream can produce a
      // silent capture track on the very first getUserMedia call.  If
      // bidirectional audio does not flow within a short window, leave
      // and rejoin voice to re-acquire the mic (the second getUserMedia
      // on a warm device always works).
      let audioFlowing = false;

      try {
        await Promise.all([waitForAllPeerAudioFlow(alice.page, 1, 15_000), waitForAllPeerAudioFlow(bob.page, 1, 15_000)]);

        audioFlowing = true;
      } catch {
        // Silent sender detected - rejoin voice to work around Chromium bug
      }

      if (!audioFlowing) {
        // Leave voice
        await aliceRoom.disconnectButton.click();
        await bobRoom.disconnectButton.click();
        await alice.page.waitForTimeout(2_000);

        // Rejoin
        await aliceRoom.joinVoiceChannel('General');
        await bobRoom.joinVoiceChannel('General');

        await expect(
          aliceRoom.channelsSidePanel.getByText('Bob')
        ).toBeVisible({ timeout: 20_000 });

        await expect(
          bobRoom.channelsSidePanel.getByText('Alice')
        ).toBeVisible({ timeout: 20_000 });

        await waitForPeerConnected(alice.page, 30_000);
        await waitForPeerConnected(bob.page, 30_000);
        await waitForConnectedPeerCount(alice.page, 1, 30_000);
        await waitForConnectedPeerCount(bob.page, 1, 30_000);
        await waitForAudioStatsPresent(alice.page, 30_000);
        await waitForAudioStatsPresent(bob.page, 30_000);
        await alice.page.waitForTimeout(3_000);
      }

      // Final assertion - must succeed after the (optional) rejoin.
      try {
        await Promise.all([waitForAllPeerAudioFlow(alice.page, 1, 60_000), waitForAllPeerAudioFlow(bob.page, 1, 60_000)]);
      } catch (error) {
        console.log('[Alice RTC Diagnostics]\n' + await dumpRtcDiagnostics(alice.page));
        console.log('[Bob RTC Diagnostics]\n' + await dumpRtcDiagnostics(bob.page));
        throw error;
      }
    });

    await test.step('Bob still has TURN entry in localStorage', async () => {
      const stored: StoredIceServerEntry[] = await bob.page.evaluate(
        (key) => JSON.parse(localStorage.getItem(key) || '[]') as StoredIceServerEntry[],
        ICE_STORAGE_KEY
      );
      const hasTurn = stored.some(
        (entry) => entry.type === 'turn' && entry.urls === 'turn:localhost:3478'
      );

      expect(hasTurn).toBe(true);
    });
  });
});
