import fs from 'node:fs';
import path from 'node:path';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prisma } from '@documenso/prisma';
import { seedDraftDocument } from '@documenso/prisma/seed/documents';
import { seedTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type {
  TCreateEnvelopePayload,
  TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
import { expect, type Page, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType, RecipientRole } from '@prisma/client';

import { apiSignin } from '../fixtures/authentication';
import {
  clickAddMyselfButton,
  clickEnvelopeEditorStep,
  getRecipientEmailInputs,
  openDocumentEnvelopeEditor,
  openTemplateEnvelopeEditor,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';

const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const V2_API_BASE_URL = `${WEBAPP_BASE_URL}/api/v2-beta`;

const examplePdfBuffer = fs.readFileSync(path.join(__dirname, '../../../../assets/example.pdf'));

/**
 * Place a field on the PDF canvas in the envelope editor.
 */
const placeFieldOnPdf = async (root: Page, fieldName: 'Signature' | 'Text', position: { x: number; y: number }) => {
  await root.getByRole('button', { name: fieldName, exact: true }).click();

  const canvas = root.locator('.konva-container canvas').first();
  await expect(canvas).toBeVisible();
  await canvas.click({ position });
};

/**
 * Create a PENDING envelope via the V2 API with a single SIGNER recipient
 * and a SIGNATURE field, then distribute it.
 *
 * Returns the envelope ID, recipient email, team URL, and user info for navigation.
 */
const createPendingEnvelopeViaApi = async () => {
  const { user, team } = await seedUser();

  const { token } = await createApiToken({
    userId: user.id,
    teamId: team.id,
    tokenName: 'e2e-resend-test',
    expiresIn: null,
  });

  const recipientEmail = `resend-${Date.now()}@test.documenso.com`;

  // 1. Create envelope with a PDF.
  const payload = {
    type: EnvelopeType.DOCUMENT,
    title: `E2E Resend Test ${Date.now()}`,
  } satisfies TCreateEnvelopePayload;

  const formData = new FormData();
  formData.append('payload', JSON.stringify(payload));
  formData.append('files', new File([examplePdfBuffer], 'example.pdf', { type: 'application/pdf' }));

  const createRes = await fetch(`${V2_API_BASE_URL}/envelope/create`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}` },
    body: formData,
  });

  expect(createRes.ok).toBeTruthy();
  const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;

  // 2. Create a SIGNER recipient.
  const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
    envelopeId: createResponse.id,
    data: [
      {
        email: recipientEmail,
        name: 'Resend Test Signer',
        role: RecipientRole.SIGNER,
        accessAuth: [],
        actionAuth: [],
      },
    ],
  };

  const recipientsRes = await fetch(`${V2_API_BASE_URL}/envelope/recipient/create-many`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(createRecipientsRequest),
  });

  expect(recipientsRes.ok).toBeTruthy();
  const recipientsResponse = await recipientsRes.json();
  const recipients = recipientsResponse.data;

  // 3. Get envelope to find the envelope item ID.
  const getRes = await fetch(`${V2_API_BASE_URL}/envelope/${createResponse.id}`, {
    headers: { Authorization: `Bearer ${token}` },
  });

  const envelope = (await getRes.json()) as TGetEnvelopeResponse;
  const envelopeItem = envelope.envelopeItems[0];

  // 4. Create a SIGNATURE field for the recipient.
  const createFieldsRequest = {
    envelopeId: createResponse.id,
    data: [
      {
        recipientId: recipients[0].id,
        envelopeItemId: envelopeItem.id,
        type: FieldType.SIGNATURE,
        page: 1,
        positionX: 100,
        positionY: 100,
        width: 50,
        height: 50,
      },
    ],
  };

  const fieldsRes = await fetch(`${V2_API_BASE_URL}/envelope/field/create-many`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(createFieldsRequest),
  });

  expect(fieldsRes.ok).toBeTruthy();

  // 5. Distribute the envelope.
  const distributeRes = await fetch(`${V2_API_BASE_URL}/envelope/distribute`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      envelopeId: createResponse.id,
    } satisfies TDistributeEnvelopeRequest),
  });

  expect(distributeRes.ok).toBeTruthy();

  return {
    user,
    team,
    envelopeId: createResponse.id,
    recipientEmail,
  };
};

test.describe('document editor', () => {
  test('send document via email', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);

    // Add the current user as a recipient via the UI.
    await clickAddMyselfButton(surface.root);
    await expect(getRecipientEmailInputs(surface.root).first()).toHaveValue(surface.userEmail);

    // Navigate to the add fields step and place a signature field.
    await clickEnvelopeEditorStep(surface.root, 'addFields');
    await expect(surface.root.getByText('Selected Recipient')).toBeVisible();
    await expect(surface.root.locator('.konva-container canvas').first()).toBeVisible();

    await placeFieldOnPdf(surface.root, 'Signature', { x: 120, y: 140 });
    await expect(surface.root.getByText('1 Field')).toBeVisible();

    // Navigate back to the recipients step so the sidebar actions are available.
    await clickEnvelopeEditorStep(surface.root, 'upload');

    // Click the "Send Document" sidebar action.
    await page.locator('button[title="Send Envelope"]').click();

    // The distribute dialog should appear.
    await expect(page.getByRole('heading', { name: 'Send Document' })).toBeVisible();

    // Click Send.
    await page.getByRole('button', { name: 'Send' }).click();

    // Assert toast appears.
    await expectToastTextToBeVisible(page, 'Envelope distributed');

    // Assert the document status was changed in the database.
    const updatedEnvelope = await prisma.envelope.findUniqueOrThrow({
      where: { id: surface.envelopeId },
    });

    expect(updatedEnvelope.status).toBe(DocumentStatus.PENDING);
  });

  test('send document shows validation when signers lack signature fields', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);

    // Add the current user as a SIGNER recipient via the UI — but do NOT place any fields.
    await clickAddMyselfButton(surface.root);
    await expect(getRecipientEmailInputs(surface.root).first()).toHaveValue(surface.userEmail);

    // Click the "Send Document" sidebar action without placing signature fields.
    await page.locator('button[title="Send Envelope"]').click();

    // The distribute dialog should appear.
    await expect(page.getByRole('heading', { name: 'Send Document' })).toBeVisible();

    // The validation warning should be shown instead of the send form.
    await expect(page.getByText('The following signers are missing signature fields')).toBeVisible();

    // The "Send" button should not be visible since we're in validation mode.
    await expect(page.getByRole('button', { name: 'Send', exact: true })).not.toBeVisible();

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

    // The dialog should be closed.
    await expect(page.getByText('The following signers are missing signature fields')).not.toBeVisible();
  });

  test('resend document sends reminder', async ({ page }) => {
    const { user, team, envelopeId, recipientEmail } = await createPendingEnvelopeViaApi();

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

    await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();

    // Click the "Resend Document" sidebar action.
    await page.locator('button[title="Resend Envelope"]').click();

    // The redistribute dialog should appear.
    await expect(page.getByRole('heading', { name: 'Resend Document' })).toBeVisible();

    // The unsigned recipient should be listed.
    await expect(page.getByText(recipientEmail)).toBeVisible();

    // Select the recipient checkbox.
    await page.getByRole('checkbox').first().click();

    // Click "Send reminder".
    await page.getByRole('button', { name: 'Send reminder' }).click();

    // Assert toast appears.
    await expectToastTextToBeVisible(page, 'Envelope resent');

    // Verify a resend audit log entry was created in the database.
    const auditLog = await prisma.documentAuditLog.findFirst({
      where: {
        envelopeId,
        type: 'EMAIL_SENT',
      },
      orderBy: { createdAt: 'desc' },
    });

    expect(auditLog).not.toBeNull();
    expect((auditLog!.data as Record<string, unknown>).isResending).toBe(true);
    expect((auditLog!.data as Record<string, unknown>).recipientEmail).toBe(recipientEmail);
  });

  test('duplicate document', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);

    // Click the "Duplicate Document" sidebar action.
    await page.locator('button[title="Duplicate Envelope"]').click();

    // The duplicate dialog should appear.
    await expect(page.getByRole('heading', { name: 'Duplicate Document' })).toBeVisible();

    // Click "Duplicate".
    await page.getByRole('button', { name: 'Duplicate' }).click();

    // Assert toast appears.
    await expectToastTextToBeVisible(page, 'Document Duplicated');

    // The page should have navigated to the new document's edit page.
    await expect(page).toHaveURL(/\/documents\/.*\/edit/);

    // Verify a new envelope was created in the database.
    const envelopes = await prisma.envelope.findMany({
      where: {
        userId: surface.userId,
        teamId: surface.teamId,
        type: 'DOCUMENT',
      },
    });

    // Should have original + duplicate.
    expect(envelopes.length).toBeGreaterThanOrEqual(2);
  });

  test('duplicate document without recipients excludes recipients and fields', async ({ page }) => {
    const { user, team } = await seedUser();

    // Seed a draft document that has a recipient with a field.
    const document = await seedDraftDocument(user, team.id, ['signer@test.documenso.com'], {
      key: `dup-exclude-recipients-${Date.now()}`,
      internalVersion: 2,
    });

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

    await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();

    // Open the duplicate dialog.
    await page.locator('button[title="Duplicate Envelope"]').click();
    await expect(page.getByRole('heading', { name: 'Duplicate Document' })).toBeVisible();

    // Uncheck "Include Recipients" — this also disables and unchecks "Include Fields".
    await page.getByLabel('Include Recipients').click();
    await expect(page.getByLabel('Include Fields')).toBeDisabled();

    // Duplicate.
    await page.getByRole('button', { name: 'Duplicate' }).click();
    await expectToastTextToBeVisible(page, 'Document Duplicated');
    await expect(page).toHaveURL(/\/documents\/.*\/edit/);

    // The duplicate should have neither recipients nor fields.
    const duplicate = await prisma.envelope.findFirstOrThrow({
      where: {
        teamId: team.id,
        type: EnvelopeType.DOCUMENT,
        id: { not: document.id },
      },
      include: { recipients: true, fields: true },
      orderBy: { createdAt: 'desc' },
    });

    expect(duplicate.recipients).toHaveLength(0);
    expect(duplicate.fields).toHaveLength(0);
  });

  test('duplicate document without fields keeps recipients but excludes fields', async ({ page }) => {
    const { user, team } = await seedUser();

    // Seed a draft document that has a recipient with a field.
    const document = await seedDraftDocument(user, team.id, ['signer@test.documenso.com'], {
      key: `dup-exclude-fields-${Date.now()}`,
      internalVersion: 2,
    });

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

    await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();

    // Open the duplicate dialog.
    await page.locator('button[title="Duplicate Envelope"]').click();
    await expect(page.getByRole('heading', { name: 'Duplicate Document' })).toBeVisible();

    // Uncheck only "Include Fields" (recipients stay included).
    await page.getByLabel('Include Fields').click();

    // Duplicate.
    await page.getByRole('button', { name: 'Duplicate' }).click();
    await expectToastTextToBeVisible(page, 'Document Duplicated');
    await expect(page).toHaveURL(/\/documents\/.*\/edit/);

    // The duplicate should keep the recipient but have no fields.
    const duplicate = await prisma.envelope.findFirstOrThrow({
      where: {
        teamId: team.id,
        type: EnvelopeType.DOCUMENT,
        id: { not: document.id },
      },
      include: { recipients: true, fields: true },
      orderBy: { createdAt: 'desc' },
    });

    expect(duplicate.recipients).toHaveLength(1);
    expect(duplicate.fields).toHaveLength(0);
  });

  test('download PDF dialog shows envelope items', async ({ page }) => {
    await openDocumentEnvelopeEditor(page);

    // Click the "Download PDF" sidebar action.
    await page.locator('button[title="Download PDF"]').click();

    // The download dialog should appear.
    await expect(page.getByRole('heading', { name: 'Download Files' })).toBeVisible();
    await expect(page.getByText('Select the files you would like to download.')).toBeVisible();

    // At least one envelope item with an "Original" download button should be listed.
    await expect(page.getByRole('button', { name: 'Original' }).first()).toBeVisible();
  });

  test('delete draft document', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);
    const documentId = surface.envelopeId;

    // Click the "Delete Document" sidebar action.
    await page.locator('button[title="Delete Envelope"]').click();

    // The delete dialog should appear.
    await expect(page.getByRole('heading', { name: 'Are you sure?' })).toBeVisible();

    // For DRAFT documents no confirmation input is needed, just click "Delete".
    await page.getByRole('button', { name: 'Delete' }).click();

    // Assert toast appears.
    await expectToastTextToBeVisible(page, 'Document deleted');

    // The page should navigate back to the documents list.
    await expect(page).toHaveURL(/\/documents$/);

    // Verify the document was deleted from the database.
    const deletedDocument = await prisma.envelope.findUnique({
      where: { id: documentId },
    });

    expect(deletedDocument).toBeNull();
  });
});

test.describe('template editor', () => {
  test('create and manage direct link', async ({ page }) => {
    const { user, team } = await seedUser();

    // seedTemplate creates a template with a SIGNER recipient.
    const template = await seedTemplate({
      title: `E2E Direct Link Template ${Date.now()}`,
      userId: user.id,
      teamId: team.id,
      internalVersion: 2,
    });

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

    await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible();

    // Click the "Direct Link" sidebar action.
    await page.locator('button[title="Direct Link"]').click();

    // The onboarding dialog should appear.
    await expect(page.getByRole('heading', { name: 'Create Direct Signing Link' })).toBeVisible();

    // Click "Enable direct link signing" to proceed.
    await page.getByRole('button', { name: 'Enable direct link signing' }).click();

    // Select recipient step: click "Create one automatically".
    await page.getByRole('button', { name: 'Create one automatically' }).click();

    // Manage step should appear.
    await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();

    // The shareable link input should be present.
    await expect(page.locator('#copy-direct-link')).toBeVisible();

    // Click "Save" to persist the toggle state.
    await page.getByRole('button', { name: 'Save' }).click();

    // Assert success toast.
    await expectToastTextToBeVisible(page, 'Success');

    // Verify a TemplateDirectLink was created and enabled in the database.
    const directLink = await prisma.templateDirectLink.findFirst({
      where: { envelopeId: template.id },
    });

    expect(directLink).not.toBeNull();
    expect(directLink!.enabled).toBe(true);
  });

  test('duplicate template', async ({ page }) => {
    const surface = await openTemplateEnvelopeEditor(page);

    // Click the "Duplicate Template" sidebar action.
    await page.locator('button[title="Duplicate Envelope"]').click();

    // The duplicate dialog should appear.
    await expect(page.getByRole('heading', { name: 'Duplicate Template' })).toBeVisible();

    // Click "Duplicate".
    await page.getByRole('button', { name: 'Duplicate' }).click();

    // Assert toast appears.
    await expectToastTextToBeVisible(page, 'Template Duplicated');

    // The page should have navigated to the new template's edit page.
    await expect(page).toHaveURL(/\/templates\/.*\/edit/);

    // Verify a new envelope was created in the database.
    const envelopes = await prisma.envelope.findMany({
      where: {
        userId: surface.userId,
        teamId: surface.teamId,
        type: 'TEMPLATE',
      },
    });

    // Should have original + duplicate.
    expect(envelopes.length).toBeGreaterThanOrEqual(2);
  });
});
