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

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

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

const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'];

test.describe('User session data isolation', () => {
  test.describe.configure({ timeout: 240_000 });

  test('preserves a user saved rooms and local history across app restarts', async ({ testServer }) => {
    const suffix = uniqueName('persist');
    const userDataDir = await mkdtemp(join(tmpdir(), 'metoyou-auth-persist-'));
    const alice: TestUser = {
      username: `alice_${suffix}`,
      displayName: 'Alice',
      password: 'TestPass123!'
    };
    const aliceServerName = `Alice Session Server ${suffix}`;
    const aliceMessage = `Alice persisted message ${suffix}`;

    let client: PersistentClient | null = null;

    try {
      client = await launchPersistentClient(userDataDir, testServer.port);

      await test.step('Alice registers and creates local chat history', async () => {
        await registerUser(client.page, alice);
        await createServerAndSendMessage(client.page, aliceServerName, aliceMessage);
      });

      await test.step('Alice sees the same saved room and message after a full restart', async () => {
        await restartPersistentClient(client, testServer.port);
        await openApp(client.page);
        await expect(client.page).not.toHaveURL(/\/login/, { timeout: 15_000 });
        await expectSavedRoomAndHistory(client.page, aliceServerName, aliceMessage);
      });
    } finally {
      await closePersistentClient(client);
      await rm(userDataDir, { recursive: true, force: true });
    }
  });

  test('gives a new user a blank slate and restores only that user local data after account switches', async ({ testServer }) => {
    const suffix = uniqueName('isolation');
    const userDataDir = await mkdtemp(join(tmpdir(), 'metoyou-auth-isolation-'));
    const alice: TestUser = {
      username: `alice_${suffix}`,
      displayName: 'Alice',
      password: 'TestPass123!'
    };
    const bob: TestUser = {
      username: `bob_${suffix}`,
      displayName: 'Bob',
      password: 'TestPass123!'
    };
    const aliceServerName = `Alice Private Server ${suffix}`;
    const bobServerName = `Bob Private Server ${suffix}`;
    const aliceMessage = `Alice history ${suffix}`;
    const bobMessage = `Bob history ${suffix}`;

    let client: PersistentClient | null = null;

    try {
      client = await launchPersistentClient(userDataDir, testServer.port);

      await test.step('Alice creates persisted local data and verifies it survives a restart', async () => {
        await registerUser(client.page, alice);
        await createServerAndSendMessage(client.page, aliceServerName, aliceMessage);

        await restartPersistentClient(client, testServer.port);
        await openApp(client.page);
        await expectSavedRoomAndHistory(client.page, aliceServerName, aliceMessage);
      });

      await test.step('Bob starts from a blank slate in the same browser profile', async () => {
        await logoutUser(client.page);
        await registerUser(client.page, bob);
        await expectBlankSlate(client.page, [aliceServerName]);
      });

      await test.step('Bob gets only his own saved room and history after a restart', async () => {
        await createServerAndSendMessage(client.page, bobServerName, bobMessage);

        await restartPersistentClient(client, testServer.port);
        await openApp(client.page);
        await expectSavedRoomAndHistory(client.page, bobServerName, bobMessage);
        await expectSavedRoomHidden(client.page, aliceServerName);
      });

      await test.step('When Alice logs back in she sees only Alice local data, not Bob data', async () => {
        await logoutUser(client.page);
        await restartPersistentClient(client, testServer.port);
        await loginUser(client.page, alice);

        await expectSavedRoomVisible(client.page, aliceServerName);
        await expectSavedRoomHidden(client.page, bobServerName);
        await expectSavedRoomAndHistory(client.page, aliceServerName, aliceMessage);
      });
    } finally {
      await closePersistentClient(client);
      await rm(userDataDir, { recursive: true, force: true });
    }
  });
});

async function launchPersistentClient(userDataDir: string, testServerPort: number): Promise<PersistentClient> {
  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());

  return {
    context,
    page,
    userDataDir
  };
}

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

  const restartedClient = await launchPersistentClient(client.userDataDir, testServerPort);

  client.context = restartedClient.context;
  client.page = restartedClient.page;
}

async function closePersistentClient(client: PersistentClient | null): Promise<void> {
  if (!client) {
    return;
  }

  await client.context.close().catch(() => {});
}

async function openApp(page: Page): Promise<void> {
  await retryTransientNavigation(() => page.goto('/', { waitUntil: 'domcontentloaded' }));
}

async function registerUser(page: Page, user: TestUser): Promise<void> {
  const registerPage = new RegisterPage(page);

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

async function loginUser(page: Page, user: TestUser): Promise<void> {
  const loginPage = new LoginPage(page);

  await retryTransientNavigation(() => loginPage.goto());
  await loginPage.login(user.username, user.password);
  await expect(page).toHaveURL(/\/(search|room)(\/|$)/, { timeout: 15_000 });
}

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

  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 });
  await expect(loginPage.usernameInput).toBeVisible({ timeout: 10_000 });
}

async function createServerAndSendMessage(page: Page, serverName: string, messageText: string): Promise<void> {
  const searchPage = new ServerSearchPage(page);
  const messagesPage = new ChatMessagesPage(page);

  await searchPage.createServer(serverName, {
    description: `User session isolation coverage for ${serverName}`
  });

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

  await messagesPage.sendMessage(messageText);
  await expect(messagesPage.getMessageItemByText(messageText)).toBeVisible({ timeout: 20_000 });
}

async function expectSavedRoomAndHistory(page: Page, roomName: string, messageText: string): Promise<void> {
  const railRoomButton = getRailSavedRoomButton(page, roomName);
  const messagesPage = new ChatMessagesPage(page);

  await expect(railRoomButton).toBeVisible({ timeout: 20_000 });
  await page.goto('/search', { waitUntil: 'domcontentloaded' });
  const searchRoomButton = getSearchSavedRoomButton(page, roomName);

  await expect(searchRoomButton).toBeVisible({ timeout: 20_000 });
  await searchRoomButton.click();
  await expect(page).toHaveURL(/\/room\//, { timeout: 20_000 });
  await expect(messagesPage.getMessageItemByText(messageText)).toBeVisible({ timeout: 20_000 });
}

async function expectBlankSlate(page: Page, hiddenRoomNames: string[]): Promise<void> {
  const searchPage = new ServerSearchPage(page);

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

  for (const roomName of hiddenRoomNames) {
    await expectSavedRoomHidden(page, roomName);
  }
}

async function expectSavedRoomVisible(page: Page, roomName: string): Promise<void> {
  await expect(getRailSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 });
  await page.goto('/search', { waitUntil: 'domcontentloaded' });
  await expect(getSearchSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 });
}

async function expectSavedRoomHidden(page: Page, roomName: string): Promise<void> {
  await expect(getRailSavedRoomButton(page, roomName)).toHaveCount(0);

  if (!page.url().includes('/search')) {
    await page.goto('/search', { waitUntil: 'domcontentloaded' });
  }

  await expect(getSearchSavedRoomButton(page, roomName)).toHaveCount(0);
}

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

function getSearchSavedRoomButton(page: Page, roomName: string) {
  return page.locator('app-server-search').getByRole('button', { name: roomName, exact: true });
}

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

  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    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`);
}

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