import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FIELD_SIGNATURE_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import { prisma } from '@documenso/prisma';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
import { customAlphabet } from 'nanoid';

import { apiSignin } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';

// Duped from `packages/lib/utils/teams.ts` due to errors when importing that file.
const formatDocumentsPath = (teamUrl: string) => `/t/${teamUrl}/documents`;
const formatTemplatesPath = (teamUrl: string) => `/t/${teamUrl}/templates`;

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

const expectSigningRequestJobForRecipient = async (recipientId: number) => {
  const job = await prisma.backgroundJob.findFirst({
    where: {
      jobId: 'send.signing.requested.email',
      payload: {
        path: ['recipientId'],
        equals: recipientId,
      },
    },
  });

  expect(job).not.toBeNull();
};

const seedSignatureFieldForRecipient = async (options: {
  envelopeId: string;
  recipientId: number;
  positionY: number;
}) => {
  const envelopeItem = await prisma.envelopeItem.findFirstOrThrow({
    where: { envelopeId: options.envelopeId },
  });

  return await prisma.field.create({
    data: {
      envelopeId: options.envelopeId,
      envelopeItemId: envelopeItem.id,
      recipientId: options.recipientId,
      type: FieldType.SIGNATURE,
      page: 1,
      positionX: 5,
      positionY: options.positionY,
      width: 20,
      height: 5,
      customText: '',
      inserted: false,
      fieldMeta: FIELD_SIGNATURE_META_DEFAULT_VALUES,
    },
  });
};

test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) => {
  const { team, owner, organisation } = await seedTeam({
    createTeamMembers: 1,
  });

  // Should be visible to team members.
  const teamTemplate = await seedTemplate({
    title: 'Team template 1',
    userId: owner.id,
    teamId: team.id,
  });

  await apiSignin({
    page,
    email: owner.email,
    redirectPath: `/t/${team.url}/templates`,
  });

  const url = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${teamTemplate.id}`;

  // Owner should see list of templates with no direct link badge.
  await page.goto(url);
  await expect(page.getByRole('button', { name: 'direct link' })).toHaveCount(1);

  // Create direct link.
  await page.getByRole('button', { name: 'Create Direct Link' }).click();
  await page.getByRole('button', { name: 'Enable direct link signing' }).click();
  await page.getByRole('button', { name: 'Create one automatically' }).click();
  await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();

  await page.waitForTimeout(1000);
  await page.getByTestId('btn-dialog-close').click();

  // Expect badge to appear.
  await expect(page.getByRole('button', { name: 'direct link' })).toHaveCount(2);
});

test('[DIRECT_TEMPLATES]: toggle direct template link', async ({ page }) => {
  const { team, owner, organisation } = await seedTeam({
    createTeamMembers: 1,
  });

  // Should be visible to team members.
  const template = await seedDirectTemplate({
    title: 'Team direct template link 1',
    userId: owner.id,
    teamId: team.id,
  });

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

  // Check that the direct template link is accessible.
  await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
  await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();

  // Navigate to template settings and disable access.
  await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}${formatTemplatesPath(template.team?.url)}`);
  await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click();
  await page.getByTestId('template-direct-link').click();
  await page.getByRole('switch').click();
  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByText('Direct link signing has been').first()).toBeVisible();

  // Check that the direct template link is no longer accessible.
  await page.goto(formatDirectTemplatePath(template.directLink?.token || '123'));
  await expect(page.getByText('404 not found')).toBeVisible();
});

test('[DIRECT_TEMPLATES]: delete direct template link', async ({ page }) => {
  const { team, owner, organisation } = await seedTeam({
    createTeamMembers: 1,
  });

  // Should be visible to team members.
  const template = await seedDirectTemplate({
    title: 'Team direct template link 1',
    userId: owner.id,
    teamId: team.id,
  });

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

  // Check that the direct template link is accessible.
  await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
  await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();

  // Navigate to template settings and delete the access.
  await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}${formatTemplatesPath(template.team?.url)}`);
  await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click();
  await page.getByTestId('template-direct-link').click();
  await page.getByRole('button', { name: 'Remove' }).click();
  await page.getByRole('button', { name: 'Confirm' }).click();
  await expect(page.getByText('Direct template link deleted').first()).toBeVisible();

  // Check that the direct template link is no longer accessible.
  await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
  await expect(page.getByText('404 not found')).toBeVisible();
});

test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page }) => {
  const { user, team } = await seedUser();

  const directTemplateWithAuth = await seedDirectTemplate({
    title: 'Personal direct template link',
    userId: user.id,
    teamId: team.id,
    createTemplateOptions: {
      authOptions: createDocumentAuthOptions({
        globalAccessAuth: ['ACCOUNT'],
        globalActionAuth: [],
      }),
    },
  });

  const directTemplatePath = formatDirectTemplatePath(directTemplateWithAuth.directLink?.token || '');

  await page.goto(directTemplatePath);

  await expect(page.getByText('Authentication required')).toBeVisible();

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

  await page.goto(directTemplatePath);

  await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
  await expect(page.getByLabel('Email')).toBeDisabled();

  const directSignatureField = directTemplateWithAuth.fields[0];

  if (!directSignatureField) {
    throw new Error('Expected seeded direct template signature field to exist');
  }

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

  await signSignaturePad(page);
  await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
  await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');

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

  await page.getByRole('button', { name: 'Sign' }).click();
  await page.waitForURL(/\/sign/);
  await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
});

test('[DIRECT_TEMPLATES]: V2 direct template link auth access', async ({ page }) => {
  const { user, team } = await seedUser();

  const directTemplateWithAuth = await seedDirectTemplate({
    title: 'Personal direct template link',
    userId: user.id,
    teamId: team.id,
    internalVersion: 2,
    createTemplateOptions: {
      authOptions: createDocumentAuthOptions({
        globalAccessAuth: ['ACCOUNT'],
        globalActionAuth: [],
      }),
    },
  });

  const directTemplatePath = formatDirectTemplatePath(directTemplateWithAuth.directLink?.token || '');

  await page.goto(directTemplatePath);

  await expect(page.getByText('Authentication required')).toBeVisible();

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

  await page.goto(directTemplatePath);

  await expect(page.getByRole('heading', { name: 'Personal direct template link' })).toBeVisible();

  const directSignatureField = directTemplateWithAuth.fields[0];

  if (!directSignatureField) {
    throw new Error('Expected seeded direct template signature field to exist');
  }

  // Wait for the PDF and the Konva canvas overlay to be ready.
  await expect(page.locator('img[data-page-number]').first()).toBeVisible({ timeout: 30_000 });
  const canvas = page.locator('.konva-container canvas').first();
  await expect(canvas).toBeVisible({ timeout: 30_000 });

  // Sign the direct template recipient's signature field via the canvas-based V2 UI.
  await signSignaturePad(page);

  const canvasBox = await canvas.boundingBox();

  if (!canvasBox) {
    throw new Error('Canvas bounding box not found');
  }

  const x =
    (Number(directSignatureField.positionX) / 100) * canvasBox.width +
    ((Number(directSignatureField.width) / 100) * canvasBox.width) / 2;
  const y =
    (Number(directSignatureField.positionY) / 100) * canvasBox.height +
    ((Number(directSignatureField.height) / 100) * canvasBox.height) / 2;

  await canvas.click({ position: { x, y } });
  await expect(page.getByText('0 Fields Remaining').first()).toBeVisible({ timeout: 10_000 });

  await page.getByRole('button', { name: 'Complete' }).click();
  await expect(page.getByLabel('Your Email')).not.toBeVisible();

  await page.getByRole('button', { name: 'Sign' }).click();
  await page.waitForURL(/\/sign/);
  await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
});

test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ page }) => {
  const { team, owner, organisation } = await seedTeam({
    createTeamMembers: 1,
  });

  // Should be visible to team members.
  const template = await seedDirectTemplate({
    title: 'Team direct template link 1',
    userId: owner.id,
    teamId: team.id,
  });

  // Check that the direct template link is accessible.
  await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
  await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();

  await page.waitForTimeout(100);
  await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());

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

  await expect(page.getByText('Next Recipient Name')).not.toBeVisible();

  const directSignatureField = template.fields[0];

  if (!directSignatureField) {
    throw new Error('Expected seeded direct template signature field to exist');
  }

  await signSignaturePad(page);
  await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
  await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');

  await page.getByRole('button', { name: 'Complete' }).click();
  await page.getByRole('button', { name: 'Sign' }).click();
  await page.waitForURL(/\/sign/);
  await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();

  // Add a longer waiting period to ensure document status is updated
  await page.waitForTimeout(3000);
});

test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with next signer dictation', async ({
  page,
}) => {
  const { team, owner, organisation } = await seedTeam({
    createTeamMembers: 1,
  });

  // Should be visible to team members.
  const template = await seedDirectTemplate({
    title: 'Team direct template link 1',
    userId: owner.id,
    teamId: team.id,
  });

  await prisma.documentMeta.update({
    where: {
      id: template.documentMetaId,
    },
    data: {
      allowDictateNextSigner: true,
      signingOrder: DocumentSigningOrder.SEQUENTIAL,
    },
  });

  // The seeded direct template already includes a signature field for the direct recipient.
  const directSignatureField = template.fields[0];

  if (!directSignatureField) {
    throw new Error('Expected seeded direct template signature field to exist');
  }

  const originalName = 'Signer 2';
  const originalSecondSignerEmail = seedTestEmail();

  // Add another signer
  const secondRecipient = await prisma.recipient.create({
    data: {
      signingOrder: 2,
      envelopeId: template.id,
      email: originalSecondSignerEmail,
      name: originalName,
      token: Math.random().toString().slice(2, 7),
      role: RecipientRole.SIGNER,
    },
  });

  await seedSignatureFieldForRecipient({
    envelopeId: template.id,
    recipientId: secondRecipient.id,
    positionY: 20,
  });

  // Check that the direct template link is accessible.
  await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
  await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();

  await page.waitForTimeout(100);
  await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());

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

  // Sign the direct template recipient's signature field via the UI.
  await signSignaturePad(page);
  await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
  await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');

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

  await expect(page.getByText('Next Recipient Name')).toBeVisible();

  const nextRecipientNameInputValue = await page.getByLabel('Next Recipient Name').inputValue();
  expect(nextRecipientNameInputValue).toBe(originalName);

  const nextRecipientEmailInputValue = await page.getByLabel('Next Recipient Email').inputValue();
  expect(nextRecipientEmailInputValue).toBe(originalSecondSignerEmail);

  const newName = 'Hello';
  const newSecondSignerEmail = seedTestEmail();

  await page.getByLabel('Next Recipient Email').fill(newSecondSignerEmail);
  await page.getByLabel('Next Recipient Name').fill(newName);

  await page.getByRole('button', { name: 'Sign' }).click();
  await page.waitForURL(/\/sign/);
  await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();

  const createdEnvelopeRecipients = await prisma.recipient.findMany({
    where: {
      envelope: {
        templateId: mapSecondaryIdToTemplateId(template.secondaryId),
      },
    },
  });

  const updatedSecondRecipient = createdEnvelopeRecipients.find((recipient) => recipient.signingOrder === 2);

  expect(updatedSecondRecipient).toBeDefined();

  if (!updatedSecondRecipient) {
    throw new Error('Expected second recipient to exist');
  }

  expect(updatedSecondRecipient.name).toBe(newName);
  expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail);
  await expectSigningRequestJobForRecipient(updatedSecondRecipient.id);
});

test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with next signer dictation', async ({
  page,
}) => {
  const { team, owner, organisation } = await seedTeam({
    createTeamMembers: 1,
  });

  // Should be visible to team members.
  const template = await seedDirectTemplate({
    title: 'Team direct template link 1',
    userId: owner.id,
    teamId: team.id,
    internalVersion: 2,
  });

  await prisma.documentMeta.update({
    where: {
      id: template.documentMetaId,
    },
    data: {
      allowDictateNextSigner: true,
      signingOrder: DocumentSigningOrder.SEQUENTIAL,
    },
  });

  // The seeded direct template already includes a signature field for the direct recipient.
  const directSignatureField = template.fields[0];

  if (!directSignatureField) {
    throw new Error('Expected seeded direct template signature field to exist');
  }

  const originalName = 'Signer 2';
  const originalSecondSignerEmail = seedTestEmail();

  // Add another signer
  const secondRecipient = await prisma.recipient.create({
    data: {
      signingOrder: 2,
      envelopeId: template.id,
      email: originalSecondSignerEmail,
      name: originalName,
      token: Math.random().toString().slice(2, 7),
      role: RecipientRole.SIGNER,
    },
  });

  await seedSignatureFieldForRecipient({
    envelopeId: template.id,
    recipientId: secondRecipient.id,
    positionY: 20,
  });

  // Check that the direct template link is accessible.
  await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
  await expect(page.getByRole('heading', { name: 'Team direct template link 1' })).toBeVisible();

  // Wait for the PDF and the Konva canvas overlay to be ready.
  await expect(page.locator('img[data-page-number]').first()).toBeVisible({ timeout: 30_000 });
  const canvas = page.locator('.konva-container canvas').first();
  await expect(canvas).toBeVisible({ timeout: 30_000 });

  // Sign the direct template recipient's signature field via the canvas-based V2 UI.
  await signSignaturePad(page);

  const canvasBox = await canvas.boundingBox();

  if (!canvasBox) {
    throw new Error('Canvas bounding box not found');
  }

  const x =
    (Number(directSignatureField.positionX) / 100) * canvasBox.width +
    ((Number(directSignatureField.width) / 100) * canvasBox.width) / 2;
  const y =
    (Number(directSignatureField.positionY) / 100) * canvasBox.height +
    ((Number(directSignatureField.height) / 100) * canvasBox.height) / 2;

  await canvas.click({ position: { x, y } });
  await expect(page.getByText('0 Fields Remaining').first()).toBeVisible({ timeout: 10_000 });

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

  const currentName = 'John Doe';
  const currentEmail = seedTestEmail();

  await page.getByPlaceholder('Enter Your Name').fill(currentName);
  await page.getByPlaceholder('Enter Your Email').fill(currentEmail);

  await expect(page.getByText('Next Recipient Name')).toBeVisible();

  const nextRecipientNameInputValue = await page.getByLabel('Next Recipient Name').inputValue();
  expect(nextRecipientNameInputValue).toBe(originalName);

  const nextRecipientEmailInputValue = await page.getByLabel('Next Recipient Email').inputValue();
  expect(nextRecipientEmailInputValue).toBe(originalSecondSignerEmail);

  const newName = 'Hello';
  const newSecondSignerEmail = seedTestEmail();

  await page.getByLabel('Next Recipient Email').fill(newSecondSignerEmail);
  await page.getByLabel('Next Recipient Name').fill(newName);

  await page.getByRole('button', { name: 'Sign' }).click();
  await page.waitForURL(/\/sign/);
  await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();

  const createdEnvelopeRecipients = await prisma.recipient.findMany({
    where: {
      envelope: {
        templateId: mapSecondaryIdToTemplateId(template.secondaryId),
      },
    },
  });

  const updatedSecondRecipient = createdEnvelopeRecipients.find((recipient) => recipient.signingOrder === 2);

  expect(updatedSecondRecipient).toBeDefined();

  if (!updatedSecondRecipient) {
    throw new Error('Expected second recipient to exist');
  }

  expect(updatedSecondRecipient.name).toBe(newName);
  expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail);
  await expectSigningRequestJobForRecipient(updatedSecondRecipient.id);
});

test('[DIRECT_TEMPLATES]: V1 direct template without signature fields shows invalid template page', async ({
  page,
}) => {
  const { user, team } = await seedUser();

  const template = await seedDirectTemplate({
    title: 'V1 invalid direct template',
    userId: user.id,
    teamId: team.id,
    createDirectRecipientSignatureField: false,
  });

  await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));

  await expect(page.getByRole('heading', { name: 'Invalid direct link template' })).toBeVisible();
  await expect(page.getByText('This direct link template cannot be used because one or more signers')).toBeVisible();

  // The signing flow must not render.
  await expect(page.getByRole('heading', { name: 'General' })).not.toBeVisible();
  await expect(page.getByRole('button', { name: 'Continue' })).not.toBeVisible();
});

test('[DIRECT_TEMPLATES]: V2 direct template without signature fields shows invalid template page', async ({
  page,
}) => {
  const { user, team } = await seedUser();

  const template = await seedDirectTemplate({
    title: 'V2 invalid direct template',
    userId: user.id,
    teamId: team.id,
    internalVersion: 2,
    createDirectRecipientSignatureField: false,
  });

  await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));

  await expect(page.getByRole('heading', { name: 'Invalid direct link template' })).toBeVisible();
  await expect(page.getByText('This direct link template cannot be used because one or more signers')).toBeVisible();

  // The signing flow (PDF canvas) must not render.
  await expect(page.locator('.konva-container canvas')).toHaveCount(0);
  await expect(page.getByRole('button', { name: 'Complete' })).not.toBeVisible();
});
