import fs from 'node:fs';
import { createTeam } from '@documenso/lib/server-only/team/create-team';
import { prisma } from '@documenso/prisma';
import { seedCompletedDocument, seedDraftDocument, seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { unzipSync } from 'fflate';

import { apiSignin, apiSignout } from '../fixtures/authentication';
import { expectToastTextToBeVisible } from '../fixtures/generic';

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

const seedBulkActionsTestRequirements = async () => {
  const sender = await seedUser({ setTeamEmailAsOwner: true });

  const [doc1, doc2, doc3] = await Promise.all([
    seedDraftDocument(sender.user, sender.team.id, [], {
      createDocumentOptions: { title: 'Bulk Test Doc 1' },
    }),
    seedDraftDocument(sender.user, sender.team.id, [], {
      createDocumentOptions: { title: 'Bulk Test Doc 2' },
    }),
    seedDraftDocument(sender.user, sender.team.id, [], {
      createDocumentOptions: { title: 'Bulk Test Doc 3' },
    }),
  ]);

  const folder = await seedBlankFolder(sender.user, sender.team.id, {
    createFolderOptions: {
      name: 'Target Folder',
      teamId: sender.team.id,
    },
  });

  return {
    sender,
    documents: [doc1, doc2, doc3],
    folder,
  };
};

test('[BULK_ACTIONS]: can select multiple documents with checkboxes', async ({ page }) => {
  const { sender } = await seedBulkActionsTestRequirements();

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await expect(page.getByText(/1\s*selected/)).toBeVisible();

  await page.locator('tr', { hasText: 'Bulk Test Doc 2' }).getByRole('checkbox').click();
  await expect(page.getByText(/2\s*selected/)).toBeVisible();
});

test('[BULK_ACTIONS]: header checkbox selects all documents on page', async ({ page }) => {
  const { sender, documents } = await seedBulkActionsTestRequirements();

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('thead').getByRole('checkbox').click();

  await expect(page.getByText(new RegExp(`${documents.length}\\s*selected`))).toBeVisible();
});

test('[BULK_ACTIONS]: can clear selection with X button', async ({ page }) => {
  const { sender } = await seedBulkActionsTestRequirements();

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('thead').getByRole('checkbox').click();
  await expect(page.getByText(/\d+\s*selected/)).toBeVisible();

  await page.getByLabel('Clear selection').click();

  await expect(page.getByText(/\d+\s*selected/)).not.toBeVisible();
});

test('[BULK_ACTIONS]: can move multiple documents to a folder', async ({ page }) => {
  const { sender, folder } = await seedBulkActionsTestRequirements();

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await page.locator('tr', { hasText: 'Bulk Test Doc 2' }).getByRole('checkbox').click();
  await page.getByRole('button', { name: 'Move', exact: true }).click();

  await expect(page.getByRole('dialog')).toBeVisible();
  await expect(page.getByText('Move Documents to Folder')).toBeVisible();

  await page.getByRole('button', { name: folder.name }).click();
  await page.getByRole('dialog').getByRole('button', { name: 'Move' }).click();

  await expectToastTextToBeVisible(page, 'Selected items have been moved.');

  await page.goto(`/t/${sender.team.url}/documents/f/${folder.id}`);
  await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).toBeVisible();
  await expect(page.getByRole('link', { name: 'Bulk Test Doc 2' })).toBeVisible();
});

test('[BULK_ACTIONS]: selection does not leak between teams', async ({ page }) => {
  const { sender } = await seedBulkActionsTestRequirements();

  const teamBUrl = `team-b-${Date.now()}`;

  await createTeam({
    userId: sender.user.id,
    teamName: 'Team B',
    teamUrl: teamBUrl,
    organisationId: sender.organisation.id,
    inheritMembers: true,
  });

  const teamB = await prisma.team.findFirstOrThrow({
    where: { url: teamBUrl },
  });

  await seedDraftDocument(sender.user, teamB.id, [], {
    createDocumentOptions: { title: 'Team B Doc' },
  });

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await expect(page.getByText(/1\s*selected/)).toBeVisible();

  // The selection made in team A must not appear in team B.
  await page.goto(`/t/${teamBUrl}/documents`);
  await expect(page.getByRole('link', { name: 'Team B Doc' })).toBeVisible();
  await expect(page.getByText(/\d+\s*selected/)).not.toBeVisible();

  // Returning to team A restores its selection.
  await page.goto(`/t/${sender.team.url}/documents`);
  await expect(page.getByText(/1\s*selected/)).toBeVisible();
});

test('[BULK_ACTIONS]: escape clears selection unless a dialog is open', async ({ page }) => {
  const { sender } = await seedBulkActionsTestRequirements();

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await expect(page.getByText(/1\s*selected/)).toBeVisible();

  // Escape while a dialog is open should close the dialog but keep the selection.
  await page.getByRole('button', { name: 'Move', exact: true }).click();
  await expect(page.getByRole('dialog')).toBeVisible();

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

  await expect(page.getByRole('dialog')).not.toBeVisible();
  await expect(page.getByText(/1\s*selected/)).toBeVisible();

  // Escape with no dialog open should clear the selection.
  await page.keyboard.press('Escape');

  await expect(page.getByText(/1\s*selected/)).not.toBeVisible();
});

test('[BULK_ACTIONS]: can bulk download multiple documents as a zip', async ({ page }) => {
  const { sender, documents } = await seedBulkActionsTestRequirements();

  const [doc1, doc2] = documents;

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await page.locator('tr', { hasText: 'Bulk Test Doc 2' }).getByRole('checkbox').click();

  await page.getByRole('button', { name: 'Download', exact: true }).click();

  const dialog = page.getByRole('dialog');

  await expect(dialog).toBeVisible();
  await expect(dialog.getByText('Download Documents')).toBeVisible();
  await expect(dialog.getByText('Bulk Test Doc 1')).toBeVisible();
  await expect(dialog.getByText('Bulk Test Doc 2')).toBeVisible();
  await expect(dialog.getByText('Draft').first()).toBeVisible();

  const downloadPromise = page.waitForEvent('download', { timeout: 10_000 });

  await dialog.getByRole('button', { name: 'Download' }).click();

  const download = await downloadPromise;

  expect(download.suggestedFilename()).toMatch(/^documenso-documents-\d{4}-\d{2}-\d{2}\.zip$/);

  const downloadPath = await download.path();
  const zipContents = unzipSync(new Uint8Array(fs.readFileSync(downloadPath)));

  // Each envelope's files are nested inside an `envelopeId_title` folder.
  expect(Object.keys(zipContents).sort()).toEqual(
    [`${doc1.id}_Bulk Test Doc 1/Bulk Test Doc 1.pdf`, `${doc2.id}_Bulk Test Doc 2/Bulk Test Doc 2.pdf`].sort(),
  );

  // Each entry should be a valid non-empty PDF (%PDF magic bytes).
  for (const entry of Object.values(zipContents)) {
    expect(Array.from(entry.slice(0, 4))).toEqual([0x25, 0x50, 0x44, 0x46]);
  }

  await expectToastTextToBeVisible(page, 'Documents downloaded');
  await expect(page.getByText(/\d+\s*selected/)).not.toBeVisible();
});

test('[BULK_ACTIONS]: can delete multiple draft documents', async ({ page }) => {
  const { sender } = await seedBulkActionsTestRequirements();

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await page.locator('tr', { hasText: 'Bulk Test Doc 2' }).getByRole('checkbox').click();

  await page.getByRole('button', { name: 'Delete' }).click();

  await expect(page.getByRole('dialog')).toBeVisible();
  await expect(page.getByText('Delete Documents')).toBeVisible();
  await expect(page.getByText('You are about to delete 2 documents')).toBeVisible();
  await expect(page.getByText('irreversible')).toBeVisible();

  await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();

  await expectToastTextToBeVisible(page, 'Documents deleted');

  await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).not.toBeVisible();
  await expect(page.getByRole('link', { name: 'Bulk Test Doc 2' })).not.toBeVisible();

  await expect(page.getByRole('link', { name: 'Bulk Test Doc 3' })).toBeVisible();
});

test('[BULK_ACTIONS]: selection clears after successful move', async ({ page }) => {
  const { sender, folder } = await seedBulkActionsTestRequirements();

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await expect(page.getByText(/1\s*selected/)).toBeVisible();

  await page.getByRole('button', { name: 'Move', exact: true }).click();
  await page.getByRole('button', { name: folder.name }).click();
  await page.getByRole('dialog').getByRole('button', { name: 'Move' }).click();

  await expectToastTextToBeVisible(page, 'Selected items have been moved.');
  await expect(page.getByText(/\d+\s*selected/)).not.toBeVisible();
});

test('[BULK_ACTIONS]: selection clears after successful delete', async ({ page }) => {
  const { sender } = await seedBulkActionsTestRequirements();

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await expect(page.getByText(/1\s*selected/)).toBeVisible();

  await page.getByRole('button', { name: 'Delete' }).click();
  await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();

  await expectToastTextToBeVisible(page, 'Documents deleted');
  await expect(page.getByText(/\d+\s*selected/)).not.toBeVisible();
});

test('[BULK_ACTIONS]: can search for folders in move dialog', async ({ page }) => {
  const { sender, folder } = await seedBulkActionsTestRequirements();

  const otherFolder = await seedBlankFolder(sender.user, sender.team.id, {
    createFolderOptions: {
      name: 'Other Folder',
      teamId: sender.team.id,
    },
  });

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();

  await page.getByRole('button', { name: 'Move', exact: true }).click();
  await expect(page.getByRole('dialog')).toBeVisible();

  await expect(page.getByRole('button', { name: folder.name })).toBeVisible();
  await expect(page.getByRole('button', { name: otherFolder.name })).toBeVisible();

  await page.getByPlaceholder('Search folders...').fill('Target');
  await expect(page.getByRole('button', { name: folder.name })).toBeVisible();
  await expect(page.getByRole('button', { name: otherFolder.name })).not.toBeVisible();

  await page.getByPlaceholder('Search folders...').fill('Other');
  await expect(page.getByRole('button', { name: folder.name })).not.toBeVisible();
  await expect(page.getByRole('button', { name: otherFolder.name })).toBeVisible();

  await page.getByPlaceholder('Search folders...').fill('NonExistent');
  await expect(page.getByText('No folders found')).toBeVisible();
});

test('[BULK_ACTIONS]: can move documents from folder to home (root)', async ({ page }) => {
  const { sender, documents, folder } = await seedBulkActionsTestRequirements();

  const { prisma } = await import('@documenso/prisma');

  await prisma.envelope.updateMany({
    where: { id: documents[0].id },
    data: { folderId: folder.id },
  });

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents/f/${folder.id}`,
  });

  await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).toBeVisible();

  await page.locator('tr', { hasText: 'Bulk Test Doc 1' }).getByRole('checkbox').click();
  await expect(page.getByText(/1\s*selected/)).toBeVisible();

  await page.getByRole('button', { name: 'Move', exact: true }).click();
  await expect(page.getByRole('dialog')).toBeVisible();

  await page.getByRole('button', { name: 'Home (No Folder)' }).click();

  await page.getByRole('dialog').getByRole('button', { name: 'Move' }).click();

  await expectToastTextToBeVisible(page, 'Selected items have been moved.');

  await page.goto(`/t/${sender.team.url}/documents`);
  await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).toBeVisible();

  await page.goto(`/t/${sender.team.url}/documents/f/${folder.id}`);
  await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).not.toBeVisible();
});

// ─── Bulk cancel ─────────────────────────────────────────────────────────────

test('[BULK_ACTIONS]: can cancel multiple pending documents', async ({ page }) => {
  const sender = await seedUser({ setTeamEmailAsOwner: true });
  const { user: recipient } = await seedUser();

  const [pending1, pending2] = await Promise.all([
    seedPendingDocument(sender.user, sender.team.id, [recipient], {
      createDocumentOptions: { title: 'Bulk Cancel Pending 1' },
    }),
    seedPendingDocument(sender.user, sender.team.id, [recipient], {
      createDocumentOptions: { title: 'Bulk Cancel Pending 2' },
    }),
  ]);

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('tr', { hasText: 'Bulk Cancel Pending 1' }).getByRole('checkbox').click();
  await page.locator('tr', { hasText: 'Bulk Cancel Pending 2' }).getByRole('checkbox').click();
  await expect(page.getByText('2 selected')).toBeVisible();

  // The bulk action bar Cancel button (distinct from the dialog's confirm button).
  await page.getByRole('button', { name: 'Cancel', exact: true }).click();

  const dialog = page.getByRole('dialog');
  await expect(dialog).toBeVisible();
  await expect(dialog.getByRole('heading', { name: 'Cancel Documents' })).toBeVisible();
  await expect(dialog.getByText('You are about to cancel 2 documents')).toBeVisible();

  await dialog.getByRole('button', { name: 'Cancel documents' }).click();

  await expectToastTextToBeVisible(page, 'Documents cancelled');

  // Selection clears after a successful cancel.
  await expect(page.getByText(/\d+ selected/)).not.toBeVisible();

  // Both documents are now cancelled in the database.
  for (const document of [pending1, pending2]) {
    const envelope = await prisma.envelope.findFirstOrThrow({
      where: { id: document.id },
      select: { status: true, deletedAt: true },
    });

    expect(envelope.status).toBe(DocumentStatus.CANCELLED);
    expect(envelope.deletedAt).toBeNull();
  }
});

test('[BULK_ACTIONS]: bulk cancel only affects pending documents', async ({ page }) => {
  const sender = await seedUser({ setTeamEmailAsOwner: true });
  const { user: recipient } = await seedUser();

  const pending = await seedPendingDocument(sender.user, sender.team.id, [recipient], {
    createDocumentOptions: { title: 'Mixed Cancel Pending' },
  });
  const draft = await seedDraftDocument(sender.user, sender.team.id, [], {
    createDocumentOptions: { title: 'Mixed Cancel Draft' },
  });
  const completed = await seedCompletedDocument(sender.user, sender.team.id, [recipient], {
    createDocumentOptions: { title: 'Mixed Cancel Completed' },
  });

  await apiSignin({
    page,
    email: sender.user.email,
    redirectPath: `/t/${sender.team.url}/documents`,
  });

  await page.locator('thead').getByRole('checkbox').click();
  await expect(page.getByText('3 selected')).toBeVisible();

  await page.getByRole('button', { name: 'Cancel', exact: true }).click();

  const dialog = page.getByRole('dialog');
  await expect(dialog).toBeVisible();
  await dialog.getByRole('button', { name: 'Cancel documents' }).click();

  // Only one of the three was pending, so this is a partial result.
  await expectToastTextToBeVisible(page, 'Documents partially cancelled');

  const pendingEnvelope = await prisma.envelope.findFirstOrThrow({
    where: { id: pending.id },
    select: { status: true },
  });
  expect(pendingEnvelope.status).toBe(DocumentStatus.CANCELLED);

  // The draft and completed documents are untouched.
  const draftEnvelope = await prisma.envelope.findFirstOrThrow({
    where: { id: draft.id },
    select: { status: true },
  });
  expect(draftEnvelope.status).toBe(DocumentStatus.DRAFT);

  const completedEnvelope = await prisma.envelope.findFirstOrThrow({
    where: { id: completed.id },
    select: { status: true },
  });
  expect(completedEnvelope.status).toBe(DocumentStatus.COMPLETED);
});

test('[BULK_ACTIONS]: a MEMBER cannot bulk cancel documents they do not own', async ({ page }) => {
  const { team, owner } = await seedTeam();

  const memberUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });

  const { user: recipient } = await seedUser();

  const ownerDocument = await seedPendingDocument(owner, team.id, [recipient], {
    createDocumentOptions: { title: 'Member Cannot Cancel This', visibility: 'EVERYONE' },
  });

  await apiSignin({
    page,
    email: memberUser.email,
    redirectPath: `/t/${team.url}/documents?status=PENDING`,
  });

  await page.locator('tr', { hasText: 'Member Cannot Cancel This' }).getByRole('checkbox').click();
  await expect(page.getByText('1 selected')).toBeVisible();

  await page.getByRole('button', { name: 'Cancel', exact: true }).click();

  const dialog = page.getByRole('dialog');
  await expect(dialog).toBeVisible();
  await dialog.getByRole('button', { name: 'Cancel documents' }).click();

  // The server rejects the cancellation for a document the MEMBER does not own,
  // so it reports zero cancelled (a partial result with the document in failedIds).
  await expectToastTextToBeVisible(page, 'Documents partially cancelled');

  // The document remains pending.
  const envelope = await prisma.envelope.findFirstOrThrow({
    where: { id: ownerDocument.id },
    select: { status: true },
  });
  expect(envelope.status).toBe(DocumentStatus.PENDING);

  await apiSignout({ page });
});
