import { expect, type Page } from '@playwright/test';
import { type Client } from '../fixtures/multi-client';
import { LoginPage } from '../pages/login.page';
import { RegisterPage } from '../pages/register.page';
import { ServerSearchPage } from '../pages/server-search.page';
import { ChatRoomPage } from '../pages/chat-room.page';
import { ChatMessagesPage } from '../pages/chat-messages.page';

export const MULTI_DEVICE_PASSWORD = 'TestPass123!';
export const MULTI_DEVICE_VOICE_CHANNEL = 'General';

export interface MultiDeviceCredentials {
  username: string;
  displayName: string;
  password: string;
}

export interface MultiDeviceScenario {
  clientA: Client;
  clientB: Client;
  credentials: MultiDeviceCredentials;
  serverName: string;
  messagesA: ChatMessagesPage;
  messagesB: ChatMessagesPage;
  roomA: ChatRoomPage;
  roomB: ChatRoomPage;
}

export function uniqueMultiDeviceName(prefix: string): string {
  return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 10_000)}`;
}

export async function createMultiDeviceScenario(
  createClient: () => Promise<Client>,
  options: { suffix?: string; serverDescription?: string } = {}
): Promise<MultiDeviceScenario> {
  const suffix = options.suffix ?? uniqueMultiDeviceName('multi-device');
  const credentials: MultiDeviceCredentials = {
    username: `multi_${suffix}`,
    displayName: 'Multi Device User',
    password: MULTI_DEVICE_PASSWORD
  };
  const serverName = `Multi Device Server ${suffix}`;
  const clientA = await createClient();
  const clientB = await createClient();

  await warmClientPage(clientA.page);
  await warmClientPage(clientB.page);

  const registerPage = new RegisterPage(clientA.page);

  await registerPage.goto();
  await registerPage.register(credentials.username, credentials.displayName, credentials.password);
  await expect(clientA.page).toHaveURL(/\/dashboard/, { timeout: 15_000 });

  const searchA = new ServerSearchPage(clientA.page);

  await searchA.createServer(serverName, {
    description: options.serverDescription ?? 'Multi-device session coverage'
  });

  await expect(clientA.page).toHaveURL(/\/room\//, { timeout: 15_000 });
  await waitForCurrentRoomName(clientA.page, serverName);

  const roomA = new ChatRoomPage(clientA.page);

  await roomA.ensureVoiceChannelExists(MULTI_DEVICE_VOICE_CHANNEL);

  await loginSecondDeviceIntoServer(clientB.page, credentials, serverName);
  await waitForCurrentRoomName(clientB.page, serverName);

  const messagesA = new ChatMessagesPage(clientA.page);
  const messagesB = new ChatMessagesPage(clientB.page);
  const roomB = new ChatRoomPage(clientB.page);

  await messagesA.waitForReady();
  await messagesB.waitForReady();

  return {
    clientA,
    clientB,
    credentials,
    serverName,
    messagesA,
    messagesB,
    roomA,
    roomB
  };
}

export async function loginSecondDeviceIntoServer(
  page: Page,
  credentials: MultiDeviceCredentials,
  serverName: string
): Promise<void> {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login(credentials.username, credentials.password);
  await expect(page).toHaveURL(/\/dashboard/, { timeout: 15_000 });

  const search = new ServerSearchPage(page);

  await search.joinServerFromSearch(serverName);
  await expect(page).toHaveURL(/\/room\//, { timeout: 20_000 });
  await expect(page.locator('app-rooms-side-panel').first()).toBeVisible({ timeout: 20_000 });
}

export async function expectCrossDeviceMessage(
  sender: ChatMessagesPage,
  receiver: ChatMessagesPage,
  message: string,
  timeout = 60_000
): Promise<void> {
  await sender.sendMessage(message);

  await expect.poll(async () => {
    return await receiver.getMessageItemByText(message).isVisible()
      .catch(() => false);
  }, { timeout }).toBe(true);
}

async function warmClientPage(page: Page): Promise<void> {
  await page.goto('/dashboard', { waitUntil: 'domcontentloaded' });
  await page.waitForLoadState('networkidle').catch(() => undefined);
}

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 }
  );
}

export async function readClientInstanceId(page: Page): Promise<string | null> {
  return page.evaluate(() => {
    const sessionId = sessionStorage.getItem('metoyou.clientInstanceId')?.trim();

    if (sessionId) {
      return sessionId;
    }

    return localStorage.getItem('metoyou.clientInstanceId')?.trim() ?? null;
  });
}

export async function logoutFromMenu(page: Page): Promise<void> {
  const menuButton = page.getByRole('button', { name: 'Menu' });
  const logoutButton = page.getByRole('button', { name: 'Logout' });

  await expect(menuButton).toBeVisible({ timeout: 10_000 });
  await menuButton.click();
  await expect(logoutButton).toBeVisible({ timeout: 10_000 });
  await logoutButton.click();
  await expect(page).toHaveURL(/\/login/, { timeout: 15_000 });
}

export function channelsSidePanel(page: Page) {
  return page.locator('app-rooms-side-panel').first();
}

export function membersSidePanel(page: Page) {
  return page.locator('app-rooms-side-panel').last();
}

export function passiveVoiceChannelJoinBadge(page: Page, channelName = MULTI_DEVICE_VOICE_CHANNEL) {
  return page
    .locator(`button[data-channel-type="voice"][data-channel-name="${channelName}"]`)
    .getByText('Join', { exact: true });
}

export async function expectPassiveVoiceOnDevice(
  page: Page,
  options: { timeout?: number; displayName?: string; channelName?: string } = {}
): Promise<void> {
  const timeout = options.timeout ?? 45_000;
  const channelName = options.channelName ?? MULTI_DEVICE_VOICE_CHANNEL;
  const displayName = options.displayName;

  await expect.poll(async () => {
    const membersLabel = await membersSidePanel(page)
      .getByText('In voice on another device', { exact: false })
      .isVisible()
      .catch(() => false);
    const joinBadge = await passiveVoiceChannelJoinBadge(page, channelName).isVisible()
      .catch(() => false);
    const grayedVoiceUser = displayName
      ? await channelsSidePanel(page).locator('.opacity-50')
        .filter({ hasText: displayName })
        .first()
        .isVisible()
        .catch(() => false)
      : false;

    return membersLabel || joinBadge || grayedVoiceUser;
  }, { timeout }).toBe(true);
}

export async function expectActiveVoiceOnDevice(page: Page, timeout = 20_000): Promise<void> {
  await expect(page.locator('app-voice-controls, app-voice-workspace').first()).toBeVisible({ timeout });
}
