import {
  expect,
  type Locator,
  type Page
} from '@playwright/test';
import { test, 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 } from '../../pages/chat-messages.page';

interface DesktopNotificationRecord {
  title: string;
  body: string;
}

interface NotificationScenario {
  alice: Client;
  bob: Client;
  aliceRoom: ChatRoomPage;
  bobRoom: ChatRoomPage;
  bobMessages: ChatMessagesPage;
  serverName: string;
  channelName: string;
}

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

  test('shows desktop notifications and unread badges for inactive channels', async ({ createClient }) => {
    const scenario = await createNotificationScenario(createClient);
    const message = `Background notification ${uniqueName('msg')}`;

    await test.step('Bob sends a message to Alice\'s inactive channel', async () => {
      await clearDesktopNotifications(scenario.alice.page);
      await scenario.bobRoom.joinTextChannel(scenario.channelName);
      await scenario.bobMessages.sendMessage(message);
    });

    await test.step('Alice receives a desktop notification with the channel preview', async () => {
      const notification = await waitForDesktopNotification(scenario.alice.page);

      expect(notification).toEqual({
        title: `${scenario.serverName} · #${scenario.channelName}`,
        body: `Bob: ${message}`
      });
    });

    await test.step('Alice sees unread badges for the room and the inactive channel', async () => {
      await expect(getUnreadBadge(getSavedRoomButton(scenario.alice.page, scenario.serverName))).toHaveText('1', { timeout: 20_000 });
      await expect(getUnreadBadge(getTextChannelButton(scenario.alice.page, scenario.channelName))).toHaveText('1', { timeout: 20_000 });
    });
  });

  test('keeps unread badges visible when a muted channel suppresses desktop popups', async ({ createClient }) => {
    const scenario = await createNotificationScenario(createClient);
    const message = `Muted notification ${uniqueName('msg')}`;

    await test.step('Alice mutes the inactive text channel', async () => {
      await muteTextChannel(scenario.alice.page, scenario.channelName);
      await clearDesktopNotifications(scenario.alice.page);
    });

    await test.step('Bob sends a message into the muted channel', async () => {
      await scenario.bobRoom.joinTextChannel(scenario.channelName);
      await scenario.bobMessages.sendMessage(message);
    });

    await test.step('Alice still sees unread badges for the room and channel', async () => {
      await expect(getUnreadBadge(getSavedRoomButton(scenario.alice.page, scenario.serverName))).toHaveText('1', { timeout: 20_000 });
      await expect(getUnreadBadge(getTextChannelButton(scenario.alice.page, scenario.channelName))).toHaveText('1', { timeout: 20_000 });
    });

    await test.step('Alice does not get a muted desktop popup', async () => {
      const notificationAppeared = await waitForAnyDesktopNotification(scenario.alice.page, 1_500);

      expect(notificationAppeared).toBe(false);
    });
  });
});

async function createNotificationScenario(createClient: () => Promise<Client>): Promise<NotificationScenario> {
  const suffix = uniqueName('notify');
  const serverName = `Notifications Server ${suffix}`;
  const channelName = uniqueName('updates');
  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 installDesktopNotificationSpy(alice.page);

  await registerUser(alice.page, aliceCredentials.username, aliceCredentials.displayName, aliceCredentials.password);
  await registerUser(bob.page, bobCredentials.username, bobCredentials.displayName, bobCredentials.password);

  const aliceSearch = new ServerSearchPage(alice.page);

  await aliceSearch.createServer(serverName, {
    description: 'E2E notification coverage server'
  });

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

  const bobSearch = new ServerSearchPage(bob.page);

  await bobSearch.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();
  await aliceRoom.ensureTextChannelExists(channelName);
  await expect(getTextChannelButton(alice.page, channelName)).toBeVisible({ timeout: 20_000 });

  return {
    alice,
    bob,
    aliceRoom,
    bobRoom,
    bobMessages,
    serverName,
    channelName
  };
}

async function registerUser(page: Page, username: string, displayName: string, password: string): Promise<void> {
  const registerPage = new RegisterPage(page);

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

async function installDesktopNotificationSpy(page: Page): Promise<void> {
  await page.addInitScript(() => {
    const notifications: DesktopNotificationRecord[] = [];

    class MockNotification {
      static permission = 'granted';

      onclick: (() => void) | null = null;

      constructor(title: string, options?: NotificationOptions) {
        notifications.push({
          title,
          body: options?.body ?? ''
        });
      }

      static async requestPermission(): Promise<NotificationPermission> {
        return 'granted';
      }

      close(): void {
        return;
      }
    }

    Object.defineProperty(window, '__desktopNotifications', {
      value: notifications,
      configurable: true
    });

    Object.defineProperty(window, 'Notification', {
      value: MockNotification,
      configurable: true,
      writable: true
    });
  });
}

async function clearDesktopNotifications(page: Page): Promise<void> {
  await page.evaluate(() => {
    (window as WindowWithDesktopNotifications).__desktopNotifications.length = 0;
  });
}

async function waitForDesktopNotification(page: Page): Promise<DesktopNotificationRecord> {
  await expect.poll(
    async () => (await readDesktopNotifications(page)).length,
    {
      timeout: 20_000,
      message: 'Expected a desktop notification to be emitted'
    }
  ).toBeGreaterThan(0);

  const notifications = await readDesktopNotifications(page);

  return notifications[notifications.length - 1];
}

async function waitForAnyDesktopNotification(page: Page, timeout: number): Promise<boolean> {
  try {
    await page.waitForFunction(
      () => (window as WindowWithDesktopNotifications).__desktopNotifications.length > 0,
      undefined,
      { timeout }
    );

    return true;
  } catch (error) {
    if (error instanceof Error && error.name === 'TimeoutError') {
      return false;
    }

    throw error;
  }
}

async function readDesktopNotifications(page: Page): Promise<DesktopNotificationRecord[]> {
  return page.evaluate(() => {
    return [...(window as WindowWithDesktopNotifications).__desktopNotifications];
  });
}

async function muteTextChannel(page: Page, channelName: string): Promise<void> {
  const channelButton = getTextChannelButton(page, channelName);
  const contextMenu = page.locator('app-context-menu');

  await expect(channelButton).toBeVisible({ timeout: 20_000 });
  await channelButton.click({ button: 'right' });
  await expect(contextMenu.getByRole('button', { name: 'Mute Notifications' })).toBeVisible({ timeout: 10_000 });
  await contextMenu.getByRole('button', { name: 'Mute Notifications' }).click();
  await expect(contextMenu).toHaveCount(0);
}

function getSavedRoomButton(page: Page, roomName: string): Locator {
  return page.locator(`button[title="${roomName}"]`).first();
}

function getTextChannelButton(page: Page, channelName: string): Locator {
  return page.locator('app-rooms-side-panel').first()
    .locator(`button[data-channel-type="text"][data-channel-name="${channelName}"]`)
    .first();
}

function getUnreadBadge(container: Locator): Locator {
  return container.locator('span.rounded-full').first();
}

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

interface WindowWithDesktopNotifications extends Window {
  __desktopNotifications: DesktopNotificationRecord[];
}
