import { expect, type Page } from '@playwright/test';
import { test, type Client } from '../../fixtures/multi-client';
import { installTestServerEndpoints, type SeededEndpointInput } from '../../helpers/seed-test-endpoint';
import { startTestServer } from '../../helpers/test-server';
import {
  dumpRtcDiagnostics,
  getConnectedPeerCount,
  installWebRTCTracking,
  waitForAllPeerAudioFlow,
  waitForAudioStatsPresent,
  waitForConnectedPeerCount,
  waitForPeerConnected
} from '../../helpers/webrtc-helpers';
import { RegisterPage } from '../../pages/register.page';
import { ServerSearchPage } from '../../pages/server-search.page';
import { ChatRoomPage } from '../../pages/chat-room.page';

const PRIMARY_SIGNAL_ID = 'e2e-test-server-a';
const SECONDARY_SIGNAL_ID = 'e2e-test-server-b';
const PRIMARY_ROOM_NAME = `Dual Signal Voice A ${Date.now()}`;
const SECONDARY_ROOM_NAME = `Dual Signal Voice B ${Date.now()}`;
const VOICE_CHANNEL = 'General';
const USER_PASSWORD = 'TestPass123!';
const USER_COUNT = 8;
const EXPECTED_REMOTE_PEERS = USER_COUNT - 1;
const STABILITY_WINDOW_MS = 20_000;

interface TestUser {
  username: string;
  displayName: string;
  password: string;
}

type TestClient = Client & {
  user: TestUser;
};

test.describe('Dual-signal multi-user voice', () => {
  test('keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds', async ({
    createClient,
    testServer
  }) => {
    test.setTimeout(720_000);

    const secondaryServer = await startTestServer();

    try {
      const endpoints: SeededEndpointInput[] = [
        {
          id: PRIMARY_SIGNAL_ID,
          name: 'E2E Signal A',
          url: testServer.url,
          isActive: true,
          status: 'online'
        },
        {
          id: SECONDARY_SIGNAL_ID,
          name: 'E2E Signal B',
          url: secondaryServer.url,
          isActive: true,
          status: 'online'
        }
      ];
      const users = buildUsers();
      const clients = await createTrackedClients(createClient, users, endpoints);

      await test.step('Register every user with both active endpoints available', async () => {
        for (const client of clients) {
          const registerPage = new RegisterPage(client.page);

          await registerPage.goto();
          await registerPage.serverSelect.selectOption(PRIMARY_SIGNAL_ID);
          await registerPage.register(client.user.username, client.user.displayName, client.user.password);
          await expect(client.page).toHaveURL(/\/dashboard/, { timeout: 20_000 });
        }
      });

      await test.step('Create primary and secondary rooms on different signal endpoints', async () => {
        const searchPage = new ServerSearchPage(clients[0].page);

        await searchPage.createServer(PRIMARY_ROOM_NAME, {
          description: 'Primary signal room for 8-user voice mesh',
          sourceId: PRIMARY_SIGNAL_ID
        });

        await expect(clients[0].page).toHaveURL(/\/room\//, { timeout: 20_000 });

        await searchPage.createServer(SECONDARY_ROOM_NAME, {
          description: 'Secondary signal room for dual-socket coverage',
          sourceId: SECONDARY_SIGNAL_ID
        });

        await expect(clients[0].page).toHaveURL(/\/room\//, { timeout: 20_000 });
      });

      await test.step('Every user joins both rooms to keep 2 signal sockets open', async () => {
        for (const client of clients.slice(1)) {
          await joinRoomFromSearch(client.page, PRIMARY_ROOM_NAME);
        }

        for (const client of clients.slice(1)) {
          await openSearchView(client.page);
          await joinRoomFromSearch(client.page, SECONDARY_ROOM_NAME);
        }

        for (const client of clients) {
          await openSavedRoomByName(client.page, PRIMARY_ROOM_NAME);
          await waitForConnectedSignalManagerCount(client.page, 2);
        }
      });

      await test.step('Create voice channel and join all 8 users', async () => {
        const hostRoom = new ChatRoomPage(clients[0].page);

        await hostRoom.ensureVoiceChannelExists(VOICE_CHANNEL);

        for (const client of clients) {
          await joinVoiceChannelUntilConnected(client.page, VOICE_CHANNEL);
        }

        for (const client of clients) {
          await waitForVoiceRosterCount(client.page, VOICE_CHANNEL, USER_COUNT);
        }
      });

      await test.step('All users discover all peers and audio flows pairwise', async () => {
        // Wait for all clients to have at least one connected peer (fast)
        await Promise.all(clients.map((client) =>
          waitForPeerConnected(client.page, 45_000)
        ));

        // Wait for all clients to have all 7 peers connected
        await Promise.all(clients.map((client) =>
          waitForConnectedPeerCount(client.page, EXPECTED_REMOTE_PEERS, 90_000)
        ));

        // Wait for audio stats to appear on all clients
        await Promise.all(clients.map((client) =>
          waitForAudioStatsPresent(client.page, 30_000)
        ));

        // Allow the mesh to settle - voice routing, allowed-peer-id
        // propagation and renegotiation all need time after the last
        // user joins.
        await clients[0].page.waitForTimeout(5_000);

        // Check bidirectional audio flow on each client
        await Promise.all(clients.map((client) =>
          waitForAllPeerAudioFlow(client.page, EXPECTED_REMOTE_PEERS, 90_000)
        ));
      });

      await test.step('Voice workspace and side panel show all 8 users on every client', async () => {
        for (const client of clients) {
          const room = new ChatRoomPage(client.page);

          await openVoiceWorkspace(client.page);
          await expect(room.voiceWorkspace).toBeVisible({ timeout: 10_000 });
          await waitForVoiceWorkspaceUserCount(client.page, USER_COUNT);
          await waitForVoiceRosterCount(client.page, VOICE_CHANNEL, USER_COUNT);
          await waitForConnectedSignalManagerCount(client.page, 2);
        }
      });

      await test.step('Voice stays stable for more than 20 seconds across both signals', async () => {
        const deadline = Date.now() + STABILITY_WINDOW_MS;

        while (Date.now() < deadline) {
          for (const client of clients) {
            await expect.poll(async () => await getConnectedPeerCount(client.page), {
              timeout: 10_000,
              intervals: [500, 1_000]
            }).toBe(EXPECTED_REMOTE_PEERS);

            await expect.poll(async () => await getConnectedSignalManagerCount(client.page), {
              timeout: 10_000,
              intervals: [500, 1_000]
            }).toBe(2);
          }

          if (Date.now() < deadline) {
            await clients[0].page.waitForTimeout(5_000);
          }
        }

        for (const client of clients) {
          try {
            await waitForAllPeerAudioFlow(client.page, EXPECTED_REMOTE_PEERS, 30_000);
          } catch (error) {
            console.log(`[${client.user.displayName} RTC]\n${await dumpRtcDiagnostics(client.page)}`);
            throw error;
          }
        }
      });

      await test.step('Mute state propagates for every user across all clients', async () => {
        for (const client of clients) {
          const room = new ChatRoomPage(client.page);

          await room.muteButton.click();
          await waitForVoiceStateAcrossPages(clients, client.user.displayName, {
            isMuted: true,
            isDeafened: false
          });

          await room.muteButton.click();
          await waitForVoiceStateAcrossPages(clients, client.user.displayName, {
            isMuted: false,
            isDeafened: false
          });
        }
      });

      await test.step('Audio still flows on all peers after mute cycling', async () => {
        for (const client of clients) {
          try {
            await waitForAllPeerAudioFlow(client.page, EXPECTED_REMOTE_PEERS, 30_000);
          } catch (error) {
            console.log(`[${client.user.displayName} post-mute RTC]\n${await dumpRtcDiagnostics(client.page)}`);
            throw error;
          }
        }
      });

      await test.step('Deafen state propagates for every user across all clients', async () => {
        for (const client of clients) {
          const room = new ChatRoomPage(client.page);

          await room.deafenButton.click();
          await client.page.waitForTimeout(500);
          await waitForVoiceStateAcrossPages(clients, client.user.displayName, {
            isMuted: true,
            isDeafened: true
          });

          await room.deafenButton.click();
          await client.page.waitForTimeout(500);
          // Un-deafen does NOT restore mute - the user stays muted
          await waitForVoiceStateAcrossPages(clients, client.user.displayName, {
            isMuted: true,
            isDeafened: false
          });
        }
      });

      await test.step('Unmute all users and verify audio flows end-to-end', async () => {
        // Every user is left muted after deafen cycling - unmute them all
        for (const client of clients) {
          const room = new ChatRoomPage(client.page);

          await room.muteButton.click();
          await waitForVoiceStateAcrossPages(clients, client.user.displayName, {
            isMuted: false,
            isDeafened: false
          });
        }

        // Final audio flow check on every peer - confirms the full
        // send/receive pipeline still works after mute+deafen cycling
        for (const client of clients) {
          try {
            await waitForAllPeerAudioFlow(client.page, EXPECTED_REMOTE_PEERS, 30_000);
          } catch (error) {
            console.log(`[${client.user.displayName} final RTC]\n${await dumpRtcDiagnostics(client.page)}`);
            throw error;
          }
        }
      });
    } finally {
      await secondaryServer.stop();
    }
  });
});

function buildUsers(): TestUser[] {
  return Array.from({ length: USER_COUNT }, (_value, index) => ({
    username: `voice8_user_${Date.now()}_${index + 1}`,
    displayName: `Voice User ${index + 1}`,
    password: USER_PASSWORD
  }));
}

async function createTrackedClients(
  createClient: () => Promise<Client>,
  users: TestUser[],
  endpoints: readonly SeededEndpointInput[]
): Promise<TestClient[]> {
  const clients: TestClient[] = [];

  for (const user of users) {
    const client = await createClient();

    await installTestServerEndpoints(client.context, endpoints);
    await installDeterministicVoiceSettings(client.page);
    await installWebRTCTracking(client.page);

    clients.push({
      ...client,
      user
    });
  }

  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 openSearchView(page: Page): Promise<void> {
  await page.goto('/servers', { waitUntil: 'domcontentloaded' });
  await expect(page.getByPlaceholder('Search servers...')).toBeVisible({ timeout: 20_000 });
}

async function joinRoomFromSearch(page: Page, roomName: string): Promise<void> {
  await page.goto('/servers', { waitUntil: 'domcontentloaded' });
  const searchInput = page.getByPlaceholder('Search servers...');

  await expect(searchInput).toBeVisible({ timeout: 20_000 });
  await searchInput.fill(roomName);

  const roomCard = page.locator('div[title]', { hasText: roomName }).first();

  await expect(roomCard).toBeVisible({ timeout: 20_000 });
  await roomCard.dblclick();
  await expect(page).toHaveURL(/\/room\//, { timeout: 20_000 });
  await expect(page.locator('app-rooms-side-panel').first()).toBeVisible({ timeout: 20_000 });
  await waitForCurrentRoomName(page, roomName);
}

async function openSavedRoomByName(page: Page, roomName: string): Promise<void> {
  const roomButton = page.locator(`button[title="${roomName}"]`);

  await expect(roomButton).toBeVisible({ timeout: 20_000 });
  await roomButton.click();
  await expect(page).toHaveURL(/\/room\//, { timeout: 20_000 });
  await expect(page.locator('app-rooms-side-panel').first()).toBeVisible({ timeout: 20_000 });
  await waitForCurrentRoomName(page, roomName);
}

async function waitForCurrentRoomName(page: Page, roomName: string, timeout = 20_000): Promise<void> {
  await page.waitForFunction(
    (expectedRoomName) => {
      interface RoomShape {
        name?: string;
      }

      interface AngularDebugApi {
        getComponent: (element: Element) => Record<string, unknown>;
      }

      const host = document.querySelector('app-rooms-side-panel');
      const debugApi = (window as { ng?: AngularDebugApi }).ng;

      if (!host || !debugApi?.getComponent) {
        return false;
      }

      const component = debugApi.getComponent(host);
      const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;

      return currentRoom?.name === expectedRoomName;
    },
    roomName,
    { timeout }
  );
}

async function openVoiceWorkspace(page: Page): Promise<void> {
  const viewButton = page.locator('app-rooms-side-panel').getByRole('button', { name: /view|open/i })
    .first();

  if (await page.locator('app-voice-workspace').isVisible()
    .catch(() => false)) {
    return;
  }

  await expect(viewButton).toBeVisible({ timeout: 10_000 });
  await viewButton.click();
}

async function joinVoiceChannelUntilConnected(page: Page, channelName: string, attempts = 3): Promise<void> {
  const room = new ChatRoomPage(page);

  let lastError: unknown;

  for (let attempt = 1; attempt <= attempts; attempt++) {
    await room.joinVoiceChannel(channelName);

    try {
      await waitForLocalVoiceChannelConnection(page, channelName, 20_000);
      await expect(room.muteButton).toBeVisible({ timeout: 10_000 });
      return;
    } catch (error) {
      lastError = error;
      await page.waitForTimeout(1_000);
    }
  }

  const diagnostics = await getVoiceJoinDiagnostics(page, channelName);
  const displayName = diagnostics.currentUser?.displayName ?? 'Unknown user';

  throw new Error([
    `Failed to connect ${displayName} to voice channel ${channelName}.`,
    lastError instanceof Error ? `Last error: ${lastError.message}` : 'Last error: unavailable',
    `Current room: ${diagnostics.currentRoom?.name ?? 'none'} (${diagnostics.currentRoom?.id ?? 'n/a'})`,
    `Current user id: ${diagnostics.currentUser?.id ?? 'none'} / ${diagnostics.currentUser?.oderId ?? 'none'}`,
    `Current user voice state: ${JSON.stringify(diagnostics.currentUser?.voiceState ?? null)}`,
    `Voice channel id: ${diagnostics.voiceChannel?.id ?? 'missing'}`,
    `Visible voice roster: ${diagnostics.voiceUsers.join(', ') || 'none'}`,
    `Connected signaling managers: ${diagnostics.connectedSignalCount}`,
    `Local voice facade state: ${JSON.stringify(diagnostics.localVoiceState)}`,
    `Voice connection error: ${diagnostics.connectionErrorMessage ?? 'none'}`
  ].join('\n'));
}

async function waitForLocalVoiceChannelConnection(page: Page, channelName: string, timeout = 20_000): Promise<void> {
  await page.waitForFunction(
    (name) => {
      interface VoiceStateShape {
        isConnected?: boolean;
        roomId?: string;
        serverId?: string;
      }

      interface UserShape {
        voiceState?: VoiceStateShape;
      }

      interface ChannelShape {
        id: string;
        name: string;
        type: 'text' | 'voice';
      }

      interface RoomShape {
        id: string;
        channels?: ChannelShape[];
      }

      interface AngularDebugApi {
        getComponent: (element: Element) => Record<string, unknown>;
      }

      const host = document.querySelector('app-rooms-side-panel');
      const debugApi = (window as { ng?: AngularDebugApi }).ng;

      if (!host || !debugApi?.getComponent) {
        return false;
      }

      const component = debugApi.getComponent(host);
      const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;
      const currentUser = (component['currentUser'] as (() => UserShape | null) | undefined)?.() ?? null;
      const voiceChannel = currentRoom?.channels?.find((channel) => channel.type === 'voice' && channel.name === name);
      const voiceState = currentUser?.voiceState;

      return !!voiceChannel
        && voiceState?.isConnected === true
        && voiceState.roomId === voiceChannel.id
        && voiceState.serverId === currentRoom.id;
    },
    channelName,
    { timeout }
  );
}

async function getVoiceJoinDiagnostics(page: Page, channelName: string): Promise<{
  connectedSignalCount: number;
  connectionErrorMessage: string | null;
  currentRoom: { id?: string; name?: string } | null;
  currentUser: { id?: string; oderId?: string; displayName?: string; voiceState?: Record<string, unknown> } | null;
  localVoiceState: {
    isVoiceConnected: boolean;
    localStreamTracks: number;
    rawMicTracks: number;
  };
  voiceChannel: { id?: string; name?: string } | null;
  voiceUsers: string[];
}> {
  return await page.evaluate((name) => {
    interface VoiceStateShape {
      isConnected?: boolean;
      isMuted?: boolean;
      isDeafened?: boolean;
      roomId?: string;
      serverId?: string;
    }

    interface UserShape {
      id?: string;
      oderId?: string;
      displayName?: string;
      voiceState?: VoiceStateShape;
    }

    interface ChannelShape {
      id: string;
      name: string;
      type: 'text' | 'voice';
    }

    interface RoomShape {
      id?: string;
      name?: string;
      channels?: ChannelShape[];
    }

    interface AngularDebugApi {
      getComponent: (element: Element) => Record<string, unknown>;
    }

    const host = document.querySelector('app-rooms-side-panel');
    const debugApi = (window as { ng?: AngularDebugApi }).ng;

    if (!host || !debugApi?.getComponent) {
      return {
        connectedSignalCount: 0,
        connectionErrorMessage: 'Angular debug API unavailable',
        currentRoom: null,
        currentUser: null,
        localVoiceState: {
          isVoiceConnected: false,
          localStreamTracks: 0,
          rawMicTracks: 0
        },
        voiceChannel: null,
        voiceUsers: []
      };
    }

    const component = debugApi.getComponent(host);
    const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;
    const currentUser = (component['currentUser'] as (() => UserShape | null) | undefined)?.() ?? null;
    const voiceChannel = currentRoom?.channels?.find((channel) => channel.type === 'voice' && channel.name === name) ?? null;
    const voiceUsers = voiceChannel
      ? ((component['voiceUsersInRoom'] as ((roomId: string) => UserShape[]) | undefined)?.(voiceChannel.id) ?? [])
        .map((user) => user.displayName ?? 'Unknown user')
      : [];
    const voiceConnection = component['voiceConnection'] as {
      getLocalStream?: () => MediaStream | null;
      getRawMicStream?: () => MediaStream | null;
      isVoiceConnected?: () => boolean;
    } | undefined;
    const realtime = component['realtime'] as {
      connectionErrorMessage?: () => string | null;
      signalingTransportHandler?: {
        getConnectedSignalingManagers?: () => { signalUrl: string }[];
      };
    } | undefined;

    return {
      connectedSignalCount: realtime?.signalingTransportHandler?.getConnectedSignalingManagers?.().length ?? 0,
      connectionErrorMessage: realtime?.connectionErrorMessage?.() ?? null,
      currentRoom,
      currentUser,
      localVoiceState: {
        isVoiceConnected: voiceConnection?.isVoiceConnected?.() ?? false,
        localStreamTracks: voiceConnection?.getLocalStream?.()?.getTracks().length ?? 0,
        rawMicTracks: voiceConnection?.getRawMicStream?.()?.getTracks().length ?? 0
      },
      voiceChannel,
      voiceUsers
    };
  }, channelName);
}

async function waitForConnectedSignalManagerCount(page: Page, expectedCount: number): Promise<void> {
  await page.waitForFunction(
    (count) => {
      interface AngularDebugApi {
        getComponent: (element: Element) => Record<string, unknown>;
      }

      const host = document.querySelector('app-rooms-side-panel');
      const debugApi = (window as { ng?: AngularDebugApi }).ng;

      if (!host || !debugApi?.getComponent) {
        return false;
      }

      const component = debugApi.getComponent(host);
      const realtime = component['realtime'] as {
        signalingTransportHandler?: {
          getConnectedSignalingManagers?: () => { signalUrl: string }[];
        };
      } | undefined;
      const countValue = realtime?.signalingTransportHandler?.getConnectedSignalingManagers?.().length ?? 0;

      return countValue === count;
    },
    expectedCount,
    { timeout: 30_000 }
  );
}

async function getConnectedSignalManagerCount(page: Page): Promise<number> {
  return await page.evaluate(() => {
    interface AngularDebugApi {
      getComponent: (element: Element) => Record<string, unknown>;
    }

    const host = document.querySelector('app-rooms-side-panel');
    const debugApi = (window as { ng?: AngularDebugApi }).ng;

    if (!host || !debugApi?.getComponent) {
      return 0;
    }

    const component = debugApi.getComponent(host);
    const realtime = component['realtime'] as {
      signalingTransportHandler?: {
        getConnectedSignalingManagers?: () => { signalUrl: string }[];
      };
    } | undefined;

    return realtime?.signalingTransportHandler?.getConnectedSignalingManagers?.().length ?? 0;
  });
}

async function waitForVoiceWorkspaceUserCount(page: Page, expectedCount: number): Promise<void> {
  await page.waitForFunction(
    (count) => {
      interface AngularDebugApi {
        getComponent: (element: Element) => Record<string, unknown>;
      }

      const host = document.querySelector('app-voice-workspace');
      const debugApi = (window as { ng?: AngularDebugApi }).ng;

      if (!host || !debugApi?.getComponent) {
        return false;
      }

      const component = debugApi.getComponent(host);
      const connectedUsers = (component['connectedVoiceUsers'] as (() => unknown[]) | undefined)?.() ?? [];

      return connectedUsers.length === count;
    },
    expectedCount,
    { timeout: 45_000 }
  );
}

async function waitForVoiceRosterCount(page: Page, channelName: string, expectedCount: number): Promise<void> {
  await page.waitForFunction(
    ({ expected, name }) => {
      interface ChannelShape {
        id: string;
        name: string;
        type: 'text' | 'voice';
      }

      interface RoomShape {
        channels?: ChannelShape[];
      }

      interface AngularDebugApi {
        getComponent: (element: Element) => Record<string, unknown>;
      }

      const host = document.querySelector('app-rooms-side-panel');
      const debugApi = (window as { ng?: AngularDebugApi }).ng;

      if (!host || !debugApi?.getComponent) {
        return false;
      }

      const component = debugApi.getComponent(host);
      const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;
      const channelId = currentRoom?.channels?.find((channel) => channel.type === 'voice' && channel.name === name)?.id;

      if (!channelId) {
        return false;
      }

      const roster = (component['voiceUsersInRoom'] as ((roomId: string) => unknown[]) | undefined)?.(channelId) ?? [];

      return roster.length === expected;
    },
    { expected: expectedCount, name: channelName },
    { timeout: 30_000 }
  );
}

async function waitForVoiceStateAcrossPages(
  clients: readonly TestClient[],
  displayName: string,
  expectedState: { isMuted: boolean; isDeafened: boolean }
): Promise<void> {
  for (const client of clients) {
    await client.page.waitForFunction(
      ({ expectedDisplayName, expectedMuted, expectedDeafened }) => {
        interface VoiceStateShape {
          isMuted?: boolean;
          isDeafened?: boolean;
        }

        interface ChannelShape {
          id: string;
          name: string;
          type: 'text' | 'voice';
        }

        interface UserShape {
          displayName: string;
          voiceState?: VoiceStateShape;
        }

        interface RoomShape {
          channels?: ChannelShape[];
        }

        interface AngularDebugApi {
          getComponent: (element: Element) => Record<string, unknown>;
        }

        const host = document.querySelector('app-rooms-side-panel');
        const debugApi = (window as { ng?: AngularDebugApi }).ng;

        if (!host || !debugApi?.getComponent) {
          return false;
        }

        const component = debugApi.getComponent(host);
        const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;
        const voiceChannel = currentRoom?.channels?.find((channel) => channel.type === 'voice');

        if (!voiceChannel) {
          return false;
        }

        const roster = (component['voiceUsersInRoom'] as ((roomId: string) => UserShape[]) | undefined)?.(voiceChannel.id) ?? [];
        const entry = roster.find((user) => user.displayName === expectedDisplayName);

        return entry?.voiceState?.isMuted === expectedMuted
          && entry?.voiceState?.isDeafened === expectedDeafened;
      },
      {
        expectedDisplayName: displayName,
        expectedMuted: expectedState.isMuted,
        expectedDeafened: expectedState.isDeafened
      },
      { timeout: 30_000 }
    );
  }
}
