import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
  chromium,
  type BrowserContext,
  type Locator,
  type Page,
  type Route
} from '@playwright/test';
import { test, expect } from '../../fixtures/multi-client';
import { installTestServerEndpoint } from '../../helpers/seed-test-endpoint';
import { installWebRTCTracking } from '../../helpers/webrtc-helpers';
import { LoginPage } from '../../pages/login.page';
import { RegisterPage } from '../../pages/register.page';
import { ServerSearchPage } from '../../pages/server-search.page';
import { ChatMessagesPage } from '../../pages/chat-messages.page';

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

interface ImageUploadPayload {
  buffer: Buffer;
  dataUrl: string;
  mimeType: string;
  name: string;
}

interface PersistentClient {
  context: BrowserContext;
  page: Page;
  user: TestUser;
  userDataDir: string;
}

const STATIC_GIF_BASE64 = 'R0lGODlhAQABAPAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
const GIF_FRAME_MARKER = Buffer.from([
  0x21,
  0xf9,
  0x04
]);
const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'];
const SERVER_ICON_SYNC_TIMEOUT_MS = 45_000;

test.describe('Server icon sync', () => {
  test.describe.configure({ timeout: 240_000 });

  test('loads the chat-server image for online, late-joining, restarted, and discovery users', async ({ testServer }) => {
    const suffix = uniqueName('server-icon');
    const serverName = `Icon Sync Server ${suffix}`;
    const icon = buildGifUpload('server-icon');
    const aliceUser: TestUser = {
      username: `alice_${suffix}`,
      displayName: 'Alice',
      password: 'TestPass123!'
    };
    const bobUser: TestUser = {
      username: `bob_${suffix}`,
      displayName: 'Bob',
      password: 'TestPass123!'
    };
    const carolUser: TestUser = {
      username: `carol_${suffix}`,
      displayName: 'Carol',
      password: 'TestPass123!'
    };
    const daveUser: TestUser = {
      username: `dave_${suffix}`,
      displayName: 'Dave',
      password: 'TestPass123!'
    };
    const clients: PersistentClient[] = [];

    try {
      const alice = await createPersistentClient(aliceUser, testServer.port);
      const bob = await createPersistentClient(bobUser, testServer.port);

      clients.push(alice, bob);

      await test.step('Alice creates a server and Bob joins before the icon changes', async () => {
        await registerUser(alice);
        await registerUser(bob);

        await new ServerSearchPage(alice.page).createServer(serverName, {
          description: 'Server icon synchronization E2E coverage'
        });

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

        await joinServerFromSearch(bob.page, serverName);
        await waitForRoomReady(alice.page);
        await waitForRoomReady(bob.page);
        await waitForConnectedPeerCount(alice.page, 1);
        await waitForConnectedPeerCount(bob.page, 1);
      });

      const roomUrl = alice.page.url();

      await test.step('Alice uploads a server icon and sees it in every owner-facing place', async () => {
        await uploadServerIconFromSettings(alice.page, serverName, icon);

        await expectServerSettingsIcon(alice.page, serverName, icon.dataUrl);
        await closeSettingsModal(alice.page);
        await expectRoomHeaderIcon(alice.page, serverName, icon.dataUrl);
        await expectRailIcon(alice.page, serverName, icon.dataUrl);
      });

      await test.step('Bob was online during the change and receives the icon live', async () => {
        await expectRoomHeaderIcon(bob.page, serverName, icon.dataUrl);
        await expectRailIcon(bob.page, serverName, icon.dataUrl);
      });

      const carol = await createPersistentClient(carolUser, testServer.port);

      clients.push(carol);

      await test.step('Carol joins after the change and loads the existing server icon', async () => {
        await registerUser(carol);
        await joinServerFromSearch(carol.page, serverName);
        await waitForRoomReady(carol.page);
        await waitForConnectedPeerCount(alice.page, 2);

        await expectRoomHeaderIcon(carol.page, serverName, icon.dataUrl);
        await expectRailIcon(carol.page, serverName, icon.dataUrl);
      });

      await test.step('Bob keeps the server icon after a full app restart', async () => {
        await restartPersistentClient(bob, testServer.port);
        await openRoomAfterRestart(bob, roomUrl);

        await expectRoomHeaderIcon(bob.page, serverName, icon.dataUrl);
        await expectRailIcon(bob.page, serverName, icon.dataUrl);
      });

      const dave = await createPersistentClient(daveUser, testServer.port);

      clients.push(dave);

      await test.step('Dave has not joined, but discovery loads the icon through a temporary peer sync', async () => {
        await registerUser(dave);
        await stripServerIconFromDirectorySearch(dave.page, serverName);
        await dave.page.goto('/search', { waitUntil: 'domcontentloaded' });
        await new ServerSearchPage(dave.page).searchInput.fill(serverName);

        await expectSearchResultIcon(dave.page, serverName, icon.dataUrl);
        await expect(dave.page).toHaveURL(/\/search/);
      });
    } finally {
      await Promise.all(
        clients.map(async (client) => {
          await closePersistentClient(client);
          await rm(client.userDataDir, { recursive: true, force: true });
        })
      );
    }
  });
});

async function createPersistentClient(user: TestUser, testServerPort: number): Promise<PersistentClient> {
  const userDataDir = await mkdtemp(join(tmpdir(), 'metoyou-server-icon-e2e-'));
  const session = await launchPersistentSession(userDataDir, testServerPort);

  return {
    context: session.context,
    page: session.page,
    user,
    userDataDir
  };
}

async function restartPersistentClient(client: PersistentClient, testServerPort: number): Promise<void> {
  await closePersistentClient(client);

  const session = await launchPersistentSession(client.userDataDir, testServerPort);

  client.context = session.context;
  client.page = session.page;
}

async function closePersistentClient(client: PersistentClient): Promise<void> {
  try {
    await client.context.close();
  } catch {
    // Ignore repeated cleanup attempts during finally.
  }
}

async function launchPersistentSession(userDataDir: string, testServerPort: number): Promise<{ context: BrowserContext; page: Page }> {
  const context = await chromium.launchPersistentContext(userDataDir, {
    args: CLIENT_LAUNCH_ARGS,
    baseURL: 'http://localhost:4200',
    permissions: ['microphone', 'camera']
  });

  await installTestServerEndpoint(context, testServerPort);

  const page = context.pages()[0] ?? (await context.newPage());

  await installWebRTCTracking(page);

  return { context, page };
}

async function registerUser(client: PersistentClient): Promise<void> {
  const registerPage = new RegisterPage(client.page);

  await retryTransientNavigation(() => registerPage.goto());
  await registerPage.register(client.user.username, client.user.displayName, client.user.password);
  await expect(client.page).toHaveURL(/\/search/, { timeout: 15_000 });
}

async function joinServerFromSearch(page: Page, serverName: string): Promise<void> {
  await new ServerSearchPage(page).joinServerFromSearch(serverName);
  await expect(page).toHaveURL(/\/room\//, { timeout: 15_000 });
}

async function openRoomAfterRestart(client: PersistentClient, roomUrl: string): Promise<void> {
  await retryTransientNavigation(() => client.page.goto(roomUrl, { waitUntil: 'domcontentloaded' }));

  if (client.page.url().includes('/login')) {
    const loginPage = new LoginPage(client.page);

    await loginPage.login(client.user.username, client.user.password);
    await expect(client.page).toHaveURL(/\/(search|room)\//, { timeout: 15_000 });
    await client.page.goto(roomUrl, { waitUntil: 'domcontentloaded' });
  }

  await waitForRoomReady(client.page);
}

async function uploadServerIconFromSettings(page: Page, serverName: string, icon: ImageUploadPayload): Promise<void> {
  await openServerSettings(page, serverName);

  const fileInput = page.locator('#server-icon-upload');

  await expect(fileInput).toBeAttached({ timeout: 10_000 });
  await fileInput.setInputFiles({
    name: icon.name,
    mimeType: icon.mimeType,
    buffer: icon.buffer
  });
}

async function openServerSettings(page: Page, serverName: string): Promise<void> {
  await page.locator('app-title-bar button[title="Menu"]').click();

  const titleBarMenu = page.locator('app-title-bar .absolute.right-0.top-full').first();

  await expect(titleBarMenu).toBeVisible({ timeout: 5_000 });
  await titleBarMenu.getByRole('button', { name: 'Settings' }).click();

  const dialog = page.locator('app-settings-modal');
  const serverSettingsTitle = dialog.getByRole('heading', { name: 'Server Settings' });

  try {
    await expect(serverSettingsTitle).toBeVisible({ timeout: 2_000 });
  } catch {
    await openSettingsModalThroughAngularDevMode(page);
    await expect(serverSettingsTitle).toBeVisible({ timeout: 10_000 });
  }

  const serverSelect = dialog.locator('select').first();

  if ((await serverSelect.count()) > 0) {
    await expect(serverSelect).toContainText(serverName, { timeout: 10_000 });
  }

  await dialog.getByRole('button', { name: 'Server', exact: true }).click();
  await expect(page.locator('app-server-settings')).toBeVisible({ timeout: 10_000 });
}

async function openSettingsModalThroughAngularDevMode(page: Page): Promise<void> {
  await page.evaluate(() => {
    interface SettingsModalComponentHandle {
      modal?: {
        open: (page: string) => void;
      };
    }
    interface AngularDebugApi {
      getComponent: (element: Element) => SettingsModalComponentHandle;
      applyChanges?: (component: SettingsModalComponentHandle) => void;
    }

    const host = document.querySelector('app-settings-modal');
    const debugApi = (window as Window & { ng?: AngularDebugApi }).ng;
    const component = host && debugApi?.getComponent(host);

    if (!component?.modal?.open) {
      throw new Error('Angular debug API could not open settings modal');
    }

    component.modal.open('server');
    debugApi.applyChanges?.(component);
  });
}

async function closeSettingsModal(page: Page): Promise<void> {
  await page.keyboard.press('Escape');
  await expect(page.locator('app-settings-modal').getByRole('heading', { name: 'Settings', exact: true })).not.toBeVisible({ timeout: 10_000 });
}

async function stripServerIconFromDirectorySearch(page: Page, serverName: string): Promise<void> {
  await page.route('**/api/servers**', async (route: Route) => {
    const response = await route.fetch();
    const contentType = response.headers()['content-type'] ?? '';

    if (!contentType.includes('application/json')) {
      await route.fulfill({ response });
      return;
    }

    const body = await response.json();

    if (!body || !Array.isArray(body.servers)) {
      await route.fulfill({ response, json: body });
      return;
    }

    await route.fulfill({
      response,
      json: {
        ...body,
        servers: body.servers.map((server: Record<string, unknown>) => {
          if (server['name'] !== serverName) {
            return server;
          }

          const { icon: _icon, ...serverWithoutIcon } = server;

          return serverWithoutIcon;
        })
      }
    });
  });
}

async function waitForRoomReady(page: Page): Promise<void> {
  const messagesPage = new ChatMessagesPage(page);

  await messagesPage.waitForReady();
  await expect(page.locator('app-rooms-side-panel').last()).toBeVisible({ timeout: 15_000 });
}

async function waitForConnectedPeerCount(page: Page, count: number, timeout = 30_000): Promise<void> {
  await page.waitForFunction(
    (expectedCount) => {
      const connections =
        (
          window as {
            __rtcConnections?: RTCPeerConnection[];
          }
        ).__rtcConnections ?? [];

      return connections.filter((connection) => connection.connectionState === 'connected').length >= expectedCount;
    },
    count,
    { timeout }
  );
}

async function retryTransientNavigation<T>(navigate: () => Promise<T>, attempts = 4): Promise<T> {
  let lastError: unknown;

  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      return await navigate();
    } catch (error) {
      lastError = error;

      const message = error instanceof Error ? error.message : String(error);
      const isTransientNavigationError = message.includes('ERR_EMPTY_RESPONSE') || message.includes('ERR_CONNECTION_RESET');

      if (!isTransientNavigationError || attempt === attempts) {
        throw error;
      }
    }
  }

  throw lastError instanceof Error ? lastError : new Error(`Navigation failed after ${attempts} attempts`);
}

async function expectServerSettingsIcon(page: Page, serverName: string, expectedDataUrl: string): Promise<void> {
  const settingsPanel = page.locator('app-server-settings');
  const image = settingsPanel.locator('[style*="background-image"]').first();

  await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'settings server icon');
}

async function expectRoomHeaderIcon(page: Page, serverName: string, expectedDataUrl: string): Promise<void> {
  const channelsPanel = page.locator('app-rooms-side-panel').first();
  const image = channelsPanel.locator('[style*="background-image"]').first();

  await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'room header server icon');
}

async function expectRailIcon(page: Page, serverName: string, expectedDataUrl: string): Promise<void> {
  const image = page.locator(`app-servers-rail button[title="${serverName}"] [style*="background-image"]`).first();

  await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'servers rail icon');
}

async function expectSearchResultIcon(page: Page, serverName: string, expectedDataUrl: string): Promise<void> {
  const serverCard = page.locator('app-server-search div[title]', { hasText: serverName }).first();
  const image = serverCard.locator('[style*="background-image"]').first();

  await expect(serverCard).toBeVisible({ timeout: 20_000 });
  await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'search result server icon');
}

async function expectBackgroundImageLoadedWithUrl(image: Locator, expectedDataUrl: string, label: string): Promise<void> {
  await expect
    .poll(
      async () => {
        if ((await image.count()) === 0) {
          return null;
        }

        return image.evaluate((element) => getComputedStyle(element).backgroundImage);
      },
      {
        timeout: SERVER_ICON_SYNC_TIMEOUT_MS,
        message: `${label} background should update`
      }
    )
    .toContain(expectedDataUrl);

  await expect
    .poll(
      async () => {
        if ((await image.count()) === 0) {
          return false;
        }

        return image.evaluate(
          (element) =>
            new Promise<boolean>((resolve) => {
              const backgroundImage = getComputedStyle(element).backgroundImage;
              const match = /^url\("?(.*?)"?\)$/.exec(backgroundImage);
              const img = new Image();

              if (!match?.[1]) {
                resolve(false);
                return;
              }

              img.onload = () => resolve(img.naturalWidth > 0 && img.naturalHeight > 0);
              img.onerror = () => resolve(false);
              img.src = match[1];
            })
        );
      },
      {
        timeout: SERVER_ICON_SYNC_TIMEOUT_MS,
        message: `${label} should load`
      }
    )
    .toBe(true);
}

function buildGifUpload(label: string): ImageUploadPayload {
  const baseGif = Buffer.from(STATIC_GIF_BASE64, 'base64');
  const frameStart = baseGif.indexOf(GIF_FRAME_MARKER);

  if (frameStart < 0) {
    throw new Error('Failed to locate GIF frame marker for server icon payload');
  }

  const header = baseGif.subarray(0, frameStart);
  const frame = baseGif.subarray(frameStart, baseGif.length - 1);
  const commentData = Buffer.from(label, 'ascii');
  const commentExtension = Buffer.concat([
    Buffer.from([
      0x21,
      0xfe,
      commentData.length
    ]),
    commentData,
    Buffer.from([0x00])
  ]);
  const buffer = Buffer.concat([
    header,
    commentExtension,
    frame,
    frame,
    Buffer.from([0x3b])
  ]);
  const base64 = buffer.toString('base64');

  return {
    buffer,
    dataUrl: `data:image/gif;base64,${base64}`,
    mimeType: 'image/gif',
    name: `server-icon-${label}.gif`
  };
}

function uniqueName(prefix: string): string {
  return `${prefix}-${Date.now()}-${Math.random().toString(36)
    .slice(2, 8)}`;
}
