import { seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { customAlphabet } from 'nanoid';

import { apiSignin } from '../fixtures/authentication';
import { openCommandMenu } from '../fixtures/command-menu';

test.describe.configure({ mode: 'parallel' });

const nanoid = customAlphabet('1234567890abcdef', 10);

const ADMIN_PROMPT_PLACEHOLDER = 'Search documents, users, organisations…';

test('[ADMIN][GLOBAL_SEARCH]: numeric query shows verified user result and navigates', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });
  const { user: targetUser } = await seedUser();

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(String(targetUser.id));

  await expect(page.getByText('Global Users', { exact: true })).toBeVisible();

  // The category chips include the admin groups with their result counts.
  await expect(page.getByRole('button', { name: /Global Users/ })).toBeVisible();

  const userOption = page.getByRole('option').filter({ hasText: targetUser.email }).first();

  // Admin results are real links so they support native link behaviour such
  // as opening in a new tab.
  await expect(userOption.getByRole('link')).toHaveAttribute('href', `/admin/users/${targetUser.id}`);

  await userOption.click();

  await page.waitForURL(`/admin/users/${targetUser.id}`);
});

test('[ADMIN][GLOBAL_SEARCH]: numeric query shows verified team result and navigates', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });
  const { team: targetTeam } = await seedUser();

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(String(targetTeam.id));

  await expect(page.getByText('Global Teams', { exact: true })).toBeVisible();

  await page.getByRole('option').filter({ hasText: targetTeam.url }).first().click();

  await page.waitForURL(`/admin/teams/${targetTeam.id}`);
});

test('[ADMIN][GLOBAL_SEARCH]: text query shows document result and navigates', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });
  const { user: sender, team } = await seedUser();

  const document = await seedPendingDocument(sender, team.id, [], {
    createDocumentOptions: { title: `admin-ui-search-${nanoid()}` },
  });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(document.title);

  await expect(page.getByText('Global Documents', { exact: true })).toBeVisible();

  await page.getByRole('option').filter({ hasText: document.secondaryId }).first().click();

  await page.waitForURL(`/admin/documents/${document.id}`);
});

test('[ADMIN][GLOBAL_SEARCH]: envelope_ prefixed query resolves exact document', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });
  const { user: sender, team } = await seedUser();

  const document = await seedPendingDocument(sender, team.id, [], {
    createDocumentOptions: { title: `admin-ui-search-${nanoid()}` },
  });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(document.id);

  await expect(page.getByText('Global Documents', { exact: true })).toBeVisible();
  await expect(page.getByRole('option').filter({ hasText: document.title }).first()).toBeVisible();
});

test('[ADMIN][GLOBAL_SEARCH]: admin search requires more than 3 characters unless numeric', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  const adminSearchRequests: string[] = [];

  page.on('request', (request) => {
    if (request.url().includes('admin.search')) {
      adminSearchRequests.push(request.url());
    }
  });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  const input = page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first();

  // A 3 character non-numeric query must not trigger the admin search. The
  // personal document search fires for any non-empty query, so its response
  // is the synchronization anchor proving the debounced queries have fired.
  const documentSearchResponse = page.waitForResponse((response) => response.url().includes('document.search'));

  await input.fill('abc');

  await documentSearchResponse;

  await expect(page.getByText(/^Global /)).toHaveCount(0);
  expect(adminSearchRequests).toHaveLength(0);

  // A numeric query fires regardless of length.
  const adminSearchRequest = page.waitForRequest((request) => request.url().includes('admin.search'));

  await input.fill('7');

  await adminSearchRequest;
});

test('[ADMIN][GLOBAL_SEARCH]: search bar position stays fixed while searching', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });
  const { user: targetUser } = await seedUser();

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  const input = page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first();

  const initialY = (await input.boundingBox())?.y;

  expect(initialY).toBeGreaterThan(0);

  // The height of the prompt may change as results come and go, but the
  // search bar must never move.
  await input.fill(String(targetUser.id));

  await expect(page.getByText('Global Users', { exact: true })).toBeVisible();

  const resultsY = (await input.boundingBox())?.y;

  expect(resultsY).toBe(initialY);

  // The search bar must not move when there are no results at all.
  await input.fill('zzzz-no-such-thing-9x7q');

  await expect(page.getByText('No results for')).toBeVisible();

  const emptyY = (await input.boundingBox())?.y;

  expect(emptyY).toBe(initialY);
});

test('[ADMIN][GLOBAL_SEARCH]: default view shows the document page links outside a team context', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  await apiSignin({ page, email: adminUser.email });

  // Admin pages have no current team, the page links must still show.
  await page.goto('/admin/stats');

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await expect(page.getByRole('option').filter({ hasText: 'All documents' })).toBeVisible();
  await expect(page.getByRole('option').filter({ hasText: 'Draft documents' })).toBeVisible();
  await expect(page.getByRole('option').filter({ hasText: 'All templates' })).toBeVisible();

  // Chips only show for categories with actual results, not for the
  // hardcoded page links.
  await expect(page.getByRole('button', { name: /^Documents/ })).toHaveCount(0);
  await expect(page.getByRole('button', { name: /^Templates/ })).toHaveCount(0);
  await expect(page.getByRole('button', { name: /^Settings/ })).toBeVisible();
});

test('[ADMIN][GLOBAL_SEARCH]: theme can be changed from the prompt', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByRole('option').filter({ hasText: 'Change theme' }).first().click();

  // The sub page has a contextual placeholder and a back option.
  await expect(page.getByPlaceholder('Search themes…')).toBeVisible();
  await expect(page.getByRole('option').filter({ hasText: 'Back' }).first()).toBeVisible();

  await expect(page.getByRole('option').filter({ hasText: 'Dark Mode' })).toBeVisible();

  await page.getByRole('option').filter({ hasText: 'Dark Mode' }).first().click();

  await expect(page.locator('html')).toHaveClass(/dark/);

  // The back option returns to the root view.
  await page.getByRole('option').filter({ hasText: 'Back' }).first().click();

  await expect(page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first()).toBeVisible();
});

test('[ADMIN][GLOBAL_SEARCH]: capped admin groups offer a view all link', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  const namePrefix = `viewall-${nanoid()}`;

  // Seed enough users sharing a name prefix to hit the 5 result cap.
  for (let i = 0; i < 5; i++) {
    await seedUser({ name: `${namePrefix}-${i}` });
  }

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(namePrefix);

  await expect(page.getByText('Global Users', { exact: true })).toBeVisible();

  const viewAllOption = page.getByRole('option').filter({ hasText: 'View all results' }).first();

  await expect(viewAllOption.getByRole('link')).toHaveAttribute(
    'href',
    `/admin/users?search=${encodeURIComponent(namePrefix)}`,
  );
});

test('[ADMIN][GLOBAL_SEARCH]: first result is highlighted after every search', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });
  const { user: firstUser } = await seedUser();
  const { user: secondUser } = await seedUser();

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  const input = page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first();

  // First search selects the first result.
  await input.fill(String(firstUser.id));

  await expect(page.getByRole('option').filter({ hasText: firstUser.email }).first()).toBeVisible();
  await expect(page.locator('[cmdk-item]').first()).toHaveAttribute('aria-selected', 'true');

  // A subsequent search with entirely new results must select the first
  // result again.
  await input.fill(String(secondUser.id));

  await expect(page.getByRole('option').filter({ hasText: secondUser.email }).first()).toBeVisible();
  await expect(page.locator('[cmdk-item]').first()).toHaveAttribute('aria-selected', 'true');
});

test('[ADMIN][GLOBAL_SEARCH]: static items match fuzzy queries', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  // "setg" is a non-contiguous abbreviation of "Settings".
  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill('setg');

  // Wait for the debounced filter to apply first, "Draft documents" can
  // never match "setg" under either matching strategy.
  await expect(page.getByRole('option').filter({ hasText: 'Draft documents' })).toHaveCount(0);

  await expect(page.getByRole('option').filter({ hasText: 'Settings' }).first()).toBeVisible();
});

test('[ADMIN][GLOBAL_SEARCH]: page scrollbar is hidden while the prompt is open', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await expect
    .poll(async () => await page.evaluate(() => getComputedStyle(document.documentElement).overflow))
    .toBe('hidden');

  await page.keyboard.press('Escape');

  await expect
    .poll(async () => await page.evaluate(() => getComputedStyle(document.documentElement).overflow))
    .toBe('visible');
});

test('[ADMIN][GLOBAL_SEARCH]: non-admin gets the prompt without the admin search', async ({ page }) => {
  const { user, team } = await seedUser({ isAdmin: false });

  const document = await seedPendingDocument(user, team.id, []);

  const adminSearchRequests: string[] = [];

  page.on('request', (request) => {
    if (request.url().includes('admin.search')) {
      adminSearchRequests.push(request.url());
    }
  });

  await apiSignin({ page, email: user.email });

  // Non-admins get the same prompt with a non-admin placeholder.
  await openCommandMenu(page, 'Type a command or search...');

  await expect(page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER)).toHaveCount(0);

  await page.getByPlaceholder('Type a command or search...').first().fill(document.title);

  // Wait for the regular (non-admin) search to resolve so we know the
  // debounced queries have fired.
  await expect(page.getByRole('option', { name: document.title })).toBeVisible();

  await expect(page.getByText(/^Global /)).toHaveCount(0);
  expect(adminSearchRequests).toHaveLength(0);
});

test('[ADMIN][GLOBAL_SEARCH]: typing on a sub page fires no search requests', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  const searchRequests: string[] = [];

  page.on('request', (request) => {
    if (/api\/trpc\/(document|template|admin)\.search/.test(request.url())) {
      searchRequests.push(request.url());
    }
  });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByRole('option').filter({ hasText: 'Change theme' }).first().click();

  const input = page.getByPlaceholder('Search themes…');

  await expect(input).toBeVisible();

  // Long enough to pass the admin search threshold if it were enabled.
  await input.fill('dark');

  // The client-side filter applying proves the typing registered.
  await expect(page.getByRole('option').filter({ hasText: 'Dark Mode' })).toBeVisible();
  await expect(page.getByRole('option').filter({ hasText: 'Light Mode' })).toHaveCount(0);

  // Wait out the 200ms search debounce with a wide margin before asserting
  // that no requests fired: there is no response to anchor on when the
  // desired behaviour is "no requests at all".
  await page.waitForTimeout(750);

  expect(searchRequests).toHaveLength(0);
});

test('[ADMIN][GLOBAL_SEARCH]: failed searches show an error state instead of no results', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  await page.route(/api\/trpc\/(document|template|admin)\.search/, async (route) => {
    await route.fulfill({ status: 500, contentType: 'application/json', body: '{}' });
  });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill('zzzz-no-such-thing-9x7q');

  // A failed search must be honest about it, not claim there are no results.
  await expect(page.getByText('Something went wrong')).toBeVisible();
  await expect(page.getByText('No results for')).toHaveCount(0);
});

test('[ADMIN][GLOBAL_SEARCH]: partial search failure still shows results with a notice', async ({ page }) => {
  const { user: adminUser, team } = await seedUser({ isAdmin: true });

  const document = await seedPendingDocument(adminUser, team.id, [], {
    createDocumentOptions: { title: `partial-fail-${nanoid()}` },
  });

  // Only the admin search fails: the personal searches succeed.
  await page.route(/api\/trpc\/admin\.search/, async (route) => {
    await route.fulfill({ status: 500, contentType: 'application/json', body: '{}' });
  });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(document.title);

  // The successful personal document search must still render its results.
  await expect(page.getByRole('option', { name: document.title })).toBeVisible();

  // The failed admin search must be flagged rather than silently dropped.
  await expect(page.getByText('Some searches failed')).toBeVisible();
});

test('[ADMIN][GLOBAL_SEARCH]: over-length query skips the admin search without erroring', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });

  const adminSearchRequests: string[] = [];

  page.on('request', (request) => {
    if (request.url().includes('admin.search')) {
      adminSearchRequests.push(request.url());
    }
  });

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  // The admin search endpoint rejects queries longer than 100 characters, so
  // the client must not send them. The personal searches accept up to 1024
  // characters and still run, anchoring the debounced query flush.
  const documentSearchResponse = page.waitForResponse((response) => response.url().includes('document.search'));

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill('a'.repeat(150));

  await documentSearchResponse;

  // The personal searches ran and found nothing: the honest empty state, with
  // no error in sight.
  await expect(page.getByText('No results for')).toBeVisible();
  await expect(page.getByText('Something went wrong')).toHaveCount(0);

  expect(adminSearchRequests).toHaveLength(0);
});

test('[ADMIN][GLOBAL_SEARCH]: capped recipients group links to the admin documents page', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });
  const { user: sender, team } = await seedUser();

  const recipientPrefix = `viewall-recipient-${nanoid()}`;

  // Seed 5 documents, each with one recipient email sharing the prefix, to
  // hit the 5 result cap on the recipients group.
  const documents = [];

  for (let i = 0; i < 5; i++) {
    documents.push(
      await seedPendingDocument(sender, team.id, [`${recipientPrefix}-${i}@test.documenso.com`], {
        createDocumentOptions: { title: `recipient-viewall-${nanoid()}` },
      }),
    );
  }

  await apiSignin({ page, email: adminUser.email });

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(recipientPrefix);

  await expect(page.getByText('Global Recipients', { exact: true })).toBeVisible();

  // Only the recipients group matches the prefix, so this is its link.
  const viewAllOption = page.getByRole('option').filter({ hasText: 'View all results' }).first();

  await expect(viewAllOption.getByRole('link')).toHaveAttribute(
    'href',
    `/admin/documents?term=${encodeURIComponent(`recipient:${recipientPrefix}`)}`,
  );

  await viewAllOption.click();

  await page.waitForURL((url) => url.pathname === '/admin/documents');

  // The term input is prefilled with the recipient query and the matching
  // documents are listed.
  await expect(page.getByPlaceholder(/Search by document title/)).toHaveValue(`recipient:${recipientPrefix}`);

  for (const document of documents) {
    await expect(page.getByRole('link', { name: document.title })).toBeVisible();
  }
});

test('[ADMIN][GLOBAL_SEARCH]: view all results updates the documents page when already on it', async ({ page }) => {
  const { user: adminUser } = await seedUser({ isAdmin: true });
  const { user: sender, team } = await seedUser();

  const recipientPrefix = `viewall-live-${nanoid()}`;

  // Seed 5 documents with recipients matching the prefix to hit the cap, and
  // one control document whose recipient does not match: with a stale
  // (unfiltered) query the control would show, with the filter it must not.
  const documents = [];

  for (let i = 0; i < 5; i++) {
    documents.push(
      await seedPendingDocument(sender, team.id, [`${recipientPrefix}-${i}@test.documenso.com`], {
        createDocumentOptions: { title: `recipient-live-${nanoid()}` },
      }),
    );
  }

  const controlDocument = await seedPendingDocument(sender, team.id, [`control-${nanoid()}@test.documenso.com`], {
    createDocumentOptions: { title: `recipient-live-control-${nanoid()}` },
  });

  await apiSignin({ page, email: adminUser.email });

  // Start ON the admin documents page: the buggy state initializer has
  // already run with an empty term.
  await page.goto('/admin/documents');

  await expect(page.getByPlaceholder(/Search by document title/)).toBeVisible();

  await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);

  await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(recipientPrefix);

  await expect(page.getByText('Global Recipients', { exact: true })).toBeVisible();

  await page.getByRole('option').filter({ hasText: 'View all results' }).first().click();

  await page.waitForURL((url) => url.searchParams.get('term') === `recipient:${recipientPrefix}`);

  // The same-route navigation must update both the input and the results.
  await expect(page.getByPlaceholder(/Search by document title/)).toHaveValue(`recipient:${recipientPrefix}`);

  await expect(page.getByRole('link', { name: documents[0].title })).toBeVisible();
  await expect(page.getByRole('link', { name: controlDocument.title })).toHaveCount(0);
});
