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

const MOCK_EMBED_URL = 'https://example.test/mock-embed';
const MOCK_EMBED_TITLE = 'Mock Embed Title';
const MOCK_EMBED_DESCRIPTION = 'Mock embed description for chat E2E coverage.';
const MOCK_GIF_IMAGE_URL = 'data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
const DELETED_MESSAGE_CONTENT = '[Message deleted]';

test.describe('Chat messaging features', () => {
  test.describe.configure({ timeout: 180_000 });

  test('shows per-server channel lists on first saved-server click', async ({ createClient }) => {
    const scenario = await createSingleClientChatScenario(createClient);
    const alphaServerName = `Alpha Server ${uniqueName('rail')}`;
    const betaServerName = `Beta Server ${uniqueName('rail')}`;
    const alphaChannelName = uniqueName('alpha-updates');
    const betaChannelName = uniqueName('beta-plans');
    const channelsPanel = scenario.room.channelsSidePanel;

    await test.step('Create first saved server with a unique text channel', async () => {
      await createServerAndOpenRoom(scenario.search, scenario.client.page, alphaServerName, 'Rail switch alpha server');
      await scenario.room.ensureTextChannelExists(alphaChannelName);
      await expect(
        channelsPanel.locator(`button[data-channel-type="text"][data-channel-name="${alphaChannelName}"]`)
      ).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Create second saved server with a different text channel', async () => {
      await createServerAndOpenRoom(scenario.search, scenario.client.page, betaServerName, 'Rail switch beta server');
      await scenario.room.ensureTextChannelExists(betaChannelName);
      await expect(
        channelsPanel.locator(`button[data-channel-type="text"][data-channel-name="${betaChannelName}"]`)
      ).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Opening first server once restores only its channels', async () => {
      await openSavedRoomByName(scenario.client.page, alphaServerName);

      await expect(
        channelsPanel.locator(`button[data-channel-type="text"][data-channel-name="${alphaChannelName}"]`)
      ).toBeVisible({ timeout: 20_000 });

      await expect(
        channelsPanel.locator(`button[data-channel-type="text"][data-channel-name="${betaChannelName}"]`)
      ).toHaveCount(0);
    });

    await test.step('Opening second server once restores only its channels', async () => {
      await openSavedRoomByName(scenario.client.page, betaServerName);

      await expect(
        channelsPanel.locator(`button[data-channel-type="text"][data-channel-name="${betaChannelName}"]`)
      ).toBeVisible({ timeout: 20_000 });

      await expect(
        channelsPanel.locator(`button[data-channel-type="text"][data-channel-name="${alphaChannelName}"]`)
      ).toHaveCount(0);
    });
  });

  test('shows local room history on first saved-server click', async ({ createClient }) => {
    const scenario = await createSingleClientChatScenario(createClient);
    const alphaServerName = `History Alpha ${uniqueName('rail')}`;
    const betaServerName = `History Beta ${uniqueName('rail')}`;
    const alphaMessage = `Alpha history message ${uniqueName('msg')}`;
    const betaMessage = `Beta history message ${uniqueName('msg')}`;

    await test.step('Create first server and send a local message', async () => {
      await createServerAndOpenRoom(scenario.search, scenario.client.page, alphaServerName, 'Rail history alpha server');
      await scenario.messages.sendMessage(alphaMessage);
      await expect(scenario.messages.getMessageItemByText(alphaMessage)).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Create second server and send a different local message', async () => {
      await createServerAndOpenRoom(scenario.search, scenario.client.page, betaServerName, 'Rail history beta server');
      await scenario.messages.sendMessage(betaMessage);
      await expect(scenario.messages.getMessageItemByText(betaMessage)).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Opening first server once restores its history immediately', async () => {
      await openSavedRoomByName(scenario.client.page, alphaServerName);
      await expect(scenario.messages.getMessageItemByText(alphaMessage)).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Opening second server once restores its history immediately', async () => {
      await openSavedRoomByName(scenario.client.page, betaServerName);
      await expect(scenario.messages.getMessageItemByText(betaMessage)).toBeVisible({ timeout: 20_000 });
    });
  });

  test('syncs messages in a newly created text channel', async ({ createClient }) => {
    const scenario = await createChatScenario(createClient);
    const channelName = uniqueName('updates');
    const aliceMessage = `Alice text channel message ${uniqueName('msg')}`;
    const bobMessage = `Bob text channel reply ${uniqueName('msg')}`;

    await test.step('Alice creates a new text channel and both users join it', async () => {
      await scenario.aliceRoom.ensureTextChannelExists(channelName);
      await scenario.aliceRoom.joinTextChannel(channelName);
      await scenario.bobRoom.joinTextChannel(channelName);
    });

    await test.step('Alice and Bob see synced messages in the new text channel', async () => {
      await scenario.aliceMessages.sendMessage(aliceMessage);
      await expect(scenario.bobMessages.getMessageItemByText(aliceMessage)).toBeVisible({ timeout: 20_000 });

      await scenario.bobMessages.sendMessage(bobMessage);
      await expect(scenario.aliceMessages.getMessageItemByText(bobMessage)).toBeVisible({ timeout: 20_000 });
    });
  });

  test('shows typing indicators to other users', async ({ createClient }) => {
    const scenario = await createChatScenario(createClient);
    const draftMessage = `Typing indicator draft ${uniqueName('draft')}`;

    await test.step('Alice starts typing in general channel', async () => {
      await scenario.aliceMessages.typeDraft(draftMessage);
    });

    await test.step('Bob sees Alice typing', async () => {
      await expect(scenario.bob.page.getByText('Alice is typing...')).toBeVisible({ timeout: 10_000 });
    });
  });

  test('edits and removes messages for both users', async ({ createClient }) => {
    const scenario = await createChatScenario(createClient);
    const originalMessage = `Editable message ${uniqueName('edit')}`;
    const updatedMessage = `Edited message ${uniqueName('edit')}`;

    await test.step('Alice sends a message and Bob receives it', async () => {
      await scenario.aliceMessages.sendMessage(originalMessage);
      await expect(scenario.bobMessages.getMessageItemByText(originalMessage)).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Alice edits the message and both users see updated content', async () => {
      await scenario.aliceMessages.editOwnMessage(originalMessage, updatedMessage);
      await expect(scenario.aliceMessages.getMessageItemByText(updatedMessage)).toBeVisible({ timeout: 20_000 });
      await expect(scenario.alice.page.getByText('(edited)')).toBeVisible({ timeout: 10_000 });
      await expect(scenario.bobMessages.getMessageItemByText(updatedMessage)).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Alice deletes the message and both users see deletion state', async () => {
      await scenario.aliceMessages.deleteOwnMessage(updatedMessage);
      await expect(scenario.aliceMessages.getMessageItemByText(DELETED_MESSAGE_CONTENT)).toBeVisible({ timeout: 20_000 });
      await expect(scenario.bobMessages.getMessageItemByText(DELETED_MESSAGE_CONTENT)).toBeVisible({ timeout: 20_000 });
    });
  });

  test('syncs image and file attachments between users', async ({ createClient }) => {
    const scenario = await createChatScenario(createClient);
    const imageName = `${uniqueName('diagram')}.svg`;
    const fileName = `${uniqueName('notes')}.txt`;
    const imageCaption = `Image upload ${uniqueName('caption')}`;
    const fileCaption = `File upload ${uniqueName('caption')}`;
    const imageAttachment = createTextFilePayload(imageName, 'image/svg+xml', buildMockSvgMarkup(imageName));
    const fileAttachment = createTextFilePayload(fileName, 'text/plain', `Attachment body for ${fileName}`);

    await test.step('Alice sends image attachment and Bob receives it', async () => {
      await scenario.aliceMessages.attachFiles([imageAttachment]);
      await scenario.aliceMessages.sendMessage(imageCaption);

      await scenario.aliceMessages.expectMessageImageLoaded(imageName);
      await expect(scenario.bobMessages.getMessageItemByText(imageCaption)).toBeVisible({ timeout: 20_000 });
      await scenario.bobMessages.expectMessageImageLoaded(imageName);
    });

    await test.step('Alice sends generic file attachment and Bob receives it', async () => {
      await scenario.aliceMessages.attachFiles([fileAttachment]);
      await scenario.aliceMessages.sendMessage(fileCaption);

      await expect(scenario.bobMessages.getMessageItemByText(fileCaption)).toBeVisible({ timeout: 20_000 });
      await expect(scenario.bob.page.getByText(fileName, { exact: false })).toBeVisible({ timeout: 20_000 });
    });
  });

  test('renders link embeds for shared links', async ({ createClient }) => {
    const scenario = await createChatScenario(createClient);
    const messageText = `Useful docs ${MOCK_EMBED_URL}`;

    await test.step('Alice shares a link in chat', async () => {
      await scenario.aliceMessages.sendMessage(messageText);
      await expect(scenario.bobMessages.getMessageItemByText(messageText)).toBeVisible({ timeout: 20_000 });
    });

    await test.step('Both users see mocked link embed metadata', async () => {
      await expect(scenario.aliceMessages.getEmbedCardByTitle(MOCK_EMBED_TITLE)).toBeVisible({ timeout: 20_000 });
      await expect(scenario.bobMessages.getEmbedCardByTitle(MOCK_EMBED_TITLE)).toBeVisible({ timeout: 20_000 });
      await expect(scenario.bob.page.getByText(MOCK_EMBED_DESCRIPTION)).toBeVisible({ timeout: 20_000 });
    });
  });

  test('sends KLIPY GIF messages with mocked API responses', async ({ createClient }) => {
    const scenario = await createChatScenario(createClient);

    await test.step('Alice opens GIF picker and sends mocked GIF', async () => {
      await scenario.aliceMessages.openGifPicker();
      await scenario.aliceMessages.selectFirstGif();
    });

    await test.step('Bob sees GIF message sync', async () => {
      await scenario.aliceMessages.expectMessageImageLoaded('KLIPY GIF');
      await scenario.bobMessages.expectMessageImageLoaded('KLIPY GIF');
    });
  });
});

interface ChatScenario {
  alice: Client;
  bob: Client;
  aliceRoom: ChatRoomPage;
  bobRoom: ChatRoomPage;
  aliceMessages: ChatMessagesPage;
  bobMessages: ChatMessagesPage;
}

interface SingleClientChatScenario {
  client: Client;
  messages: ChatMessagesPage;
  room: ChatRoomPage;
  search: ServerSearchPage;
}

async function createSingleClientChatScenario(createClient: () => Promise<Client>): Promise<SingleClientChatScenario> {
  const suffix = uniqueName('solo');
  const client = await createClient();
  const credentials = {
    username: `solo_${suffix}`,
    displayName: 'Solo',
    password: 'TestPass123!'
  };

  await installChatFeatureMocks(client.page);

  const registerPage = new RegisterPage(client.page);

  await registerPage.goto();
  await registerPage.register(
    credentials.username,
    credentials.displayName,
    credentials.password
  );

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

  return {
    client,
    messages: new ChatMessagesPage(client.page),
    room: new ChatRoomPage(client.page),
    search: new ServerSearchPage(client.page)
  };
}

async function createChatScenario(createClient: () => Promise<Client>): Promise<ChatScenario> {
  const suffix = uniqueName('chat');
  const serverName = `Chat Server ${suffix}`;
  const aliceCredentials = {
    username: `alice_${suffix}`,
    displayName: 'Alice',
    password: 'TestPass123!'
  };
  const bobCredentials = {
    username: `bob_${suffix}`,
    displayName: 'Bob',
    password: 'TestPass123!'
  };
  const alice = await createClient();
  const bob = await createClient();

  await installChatFeatureMocks(alice.page);
  await installChatFeatureMocks(bob.page);

  const aliceRegisterPage = new RegisterPage(alice.page);
  const bobRegisterPage = new RegisterPage(bob.page);

  await aliceRegisterPage.goto();
  await aliceRegisterPage.register(
    aliceCredentials.username,
    aliceCredentials.displayName,
    aliceCredentials.password
  );

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

  await bobRegisterPage.goto();
  await bobRegisterPage.register(
    bobCredentials.username,
    bobCredentials.displayName,
    bobCredentials.password
  );

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

  const aliceSearchPage = new ServerSearchPage(alice.page);

  await aliceSearchPage.createServer(serverName, {
    description: 'E2E chat server for messaging feature coverage'
  });

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

  const bobSearchPage = new ServerSearchPage(bob.page);

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

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

  await aliceMessages.waitForReady();
  await bobMessages.waitForReady();

  return {
    alice,
    bob,
    aliceRoom,
    bobRoom,
    aliceMessages,
    bobMessages
  };
}

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

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 installChatFeatureMocks(page: Page): Promise<void> {
  await page.route('**/api/klipy/config', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ enabled: true })
    });
  });

  await page.route('**/api/klipy/gifs**', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        enabled: true,
        hasNext: false,
        results: [
          {
            id: 'mock-gif-1',
            slug: 'mock-gif-1',
            title: 'Mock Celebration GIF',
            url: MOCK_GIF_IMAGE_URL,
            previewUrl: MOCK_GIF_IMAGE_URL,
            width: 64,
            height: 64
          }
        ]
      })
    });
  });

  await page.route('**/api/link-metadata**', async (route) => {
    const requestUrl = new URL(route.request().url());
    const requestedTargetUrl = requestUrl.searchParams.get('url') ?? '';

    if (requestedTargetUrl === MOCK_EMBED_URL) {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({
          title: MOCK_EMBED_TITLE,
          description: MOCK_EMBED_DESCRIPTION,
          imageUrl: MOCK_GIF_IMAGE_URL,
          siteName: 'Mock Docs'
        })
      });

      return;
    }

    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ failed: true })
    });
  });
}

function createTextFilePayload(name: string, mimeType: string, content: string): ChatDropFilePayload {
  return {
    name,
    mimeType,
    base64: Buffer.from(content, 'utf8').toString('base64')
  };
}

function buildMockSvgMarkup(label: string): string {
  return [
    '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120" viewBox="0 0 160 120">',
    '<rect width="160" height="120" rx="18" fill="#0f172a" />',
    '<circle cx="38" cy="36" r="18" fill="#38bdf8" />',
    '<rect x="66" y="28" width="64" height="16" rx="8" fill="#f8fafc" />',
    '<rect x="24" y="74" width="112" height="12" rx="6" fill="#22c55e" />',
    `<text x="24" y="104" fill="#e2e8f0" font-size="12" font-family="Arial, sans-serif">${label}</text>`,
    '</svg>'
  ].join('');
}

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