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 {
  installAutoResumeAudioContext,
  installWebRTCTracking,
  waitForConnectedPeerCount
} from '../../helpers/webrtc-helpers';

const VOICE_SETTINGS = JSON.stringify({
  inputVolume: 100,
  outputVolume: 100,
  audioBitrate: 96,
  latencyProfile: 'balanced',
  includeSystemAudio: false,
  noiseReduction: false,
  screenShareQuality: 'balanced',
  askScreenShareQuality: false
});

/**
 * Seed deterministic voice settings on a page so noise reduction and
 * input gating don't interfere with the fake audio tone.
 */
async function seedVoiceSettings(page: import('@playwright/test').Page): Promise<void> {
  await page.addInitScript((settings: string) => {
    localStorage.setItem('metoyou_voice_settings', settings);
  }, VOICE_SETTINGS);
}

/**
 * Close all of a client's RTCPeerConnections and prevent any
 * reconnection by sabotaging the SDP negotiation methods on the
 * prototype - new connections get created but can never complete ICE.
 *
 * Chromium doesn't fire `connectionstatechange` on programmatic
 * `close()`, so we dispatch the event manually so the app's recovery
 * code runs and updates the connected-peers signal.
 */
async function killAndBlockPeerConnections(page: import('@playwright/test').Page): Promise<void> {
  await page.evaluate(() => {
    // Sabotage SDP methods so no NEW connections can negotiate.
    const proto = RTCPeerConnection.prototype;

    proto.createOffer = () => Promise.reject(new DOMException('blocked', 'NotAllowedError'));
    proto.createAnswer = () => Promise.reject(new DOMException('blocked', 'NotAllowedError'));
    proto.setLocalDescription = () => Promise.reject(new DOMException('blocked', 'NotAllowedError'));
    proto.setRemoteDescription = () => Promise.reject(new DOMException('blocked', 'NotAllowedError'));

    // Close every existing connection and manually fire the event
    // Chromium omits when close() is called from JS.
    const connections = (window as { __rtcConnections?: RTCPeerConnection[] }).__rtcConnections ?? [];

    for (const pc of connections) {
      try {
        pc.close();
        pc.dispatchEvent(new Event('connectionstatechange'));
      } catch { /* already closed */ }
    }
  });
}

test.describe('Connectivity warning', () => {
  test.describe.configure({ timeout: 180_000 });

  test('shows warning icon when a peer loses all connections', async ({ createClient }) => {
    const suffix = `connwarn_${Date.now()}`;
    const serverName = `ConnWarn ${suffix}`;
    const alice = await createClient();
    const bob = await createClient();
    const charlie = await createClient();

    // ── Install WebRTC tracking & AudioContext auto-resume ──
    for (const client of [
      alice,
      bob,
      charlie
    ]) {
      await installWebRTCTracking(client.page);
      await installAutoResumeAudioContext(client.page);
      await seedVoiceSettings(client.page);
    }

    // ── Register all three users ──
    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('Register Charlie', async () => {
      const register = new RegisterPage(charlie.page);

      await register.goto();
      await register.register(`charlie_${suffix}`, 'Charlie', 'TestPass123!');
      await expectDashboardReady(charlie.page);
    });

    // ── Create server and have everyone join ──
    await test.step('Alice creates a server', async () => {
      const search = new ServerSearchPage(alice.page);

      await search.createServer(serverName);
    });

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

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

    await test.step('Charlie joins the server', async () => {
      const search = new ServerSearchPage(charlie.page);

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

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

    // ── Everyone joins voice ──
    await test.step('All three join voice', async () => {
      await aliceRoom.joinVoiceChannel('General');
      await bobRoom.joinVoiceChannel('General');
      await charlieRoom.joinVoiceChannel('General');
    });

    await test.step('All users see each other in voice', async () => {
      // Each user should see the other two in the voice channel list.
      await expect(
        aliceRoom.channelsSidePanel.getByText('Bob')
      ).toBeVisible({ timeout: 20_000 });

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

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

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

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

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

    // ── Wait for full mesh to establish ──
    await test.step('All peer connections establish', async () => {
      // Each client should have 2 connected peers (full mesh of 3).
      await waitForConnectedPeerCount(alice.page, 2, 30_000);
      await waitForConnectedPeerCount(bob.page, 2, 30_000);
      await waitForConnectedPeerCount(charlie.page, 2, 30_000);
    });

    // ── Break Charlie's connections ──
    await test.step('Kill Charlie peer connections and block reconnection', async () => {
      await killAndBlockPeerConnections(charlie.page);

      // Give the health service time to detect the desync.
      // Peer latency pings stop -> connectedPeers updates -> desyncPeerIds recalculates.
      await alice.page.waitForTimeout(15_000);
    });

    // ── Assert connectivity warnings ──
    //
    // The warning icon (lucideAlertTriangle) is a direct sibling of the
    // user-name span inside the same voice-row div.  Using the CSS
    // general-sibling combinator (~) avoids accidentally matching a
    // parent container that holds multiple rows.
    await test.step('Alice sees warning icon next to Charlie', async () => {
      const charlieWarning = aliceRoom.channelsSidePanel
        .locator('span.truncate:has-text("Charlie") ~ ng-icon[name="lucideAlertTriangle"]');

      await expect(charlieWarning).toBeVisible({ timeout: 30_000 });
    });

    await test.step('Bob sees warning icon next to Charlie', async () => {
      const charlieWarning = bobRoom.channelsSidePanel
        .locator('span.truncate:has-text("Charlie") ~ ng-icon[name="lucideAlertTriangle"]');

      await expect(charlieWarning).toBeVisible({ timeout: 30_000 });
    });

    await test.step('Alice does NOT see warning icon next to Bob', async () => {
      const bobWarning = aliceRoom.channelsSidePanel
        .locator('span.truncate:has-text("Bob") ~ ng-icon[name="lucideAlertTriangle"]');

      await expect(bobWarning).not.toBeVisible();
    });

    await test.step('Charlie sees local desync banner', async () => {
      const desyncBanner = charlie.page.locator('text=You may have connectivity issues');

      await expect(desyncBanner).toBeVisible({ timeout: 30_000 });
    });
  });
});
