import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { expect, type Page, test } from '@playwright/test';
import { FieldType } from '@prisma/client';

import {
  addEnvelopeItemPdf,
  clickAddMyselfButton,
  clickAddSignerButton,
  clickEnvelopeEditorStep,
  getEnvelopeEditorSettingsTrigger,
  getRecipientEmailInputs,
  getRecipientRemoveButtons,
  openDocumentEnvelopeEditor,
  openEmbeddedEnvelopeEditor,
  openTemplateEnvelopeEditor,
  persistEmbeddedEnvelope,
  setRecipientEmail,
  setRecipientName,
  type TEnvelopeEditorSurface,
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
import { getKonvaElementCountForPage, getKonvaTransformerNodeCountForPage } from '../fixtures/konva';

type TFieldFlowResult = {
  externalId: string;
  recipientEmail: string;
};

const TEST_FIELD_VALUES = {
  embeddedRecipient: {
    email: 'embedded-field-recipient@documenso.com',
    name: 'Embedded Field Recipient',
  },
};

const openSettingsDialog = async (root: Page) => {
  await getEnvelopeEditorSettingsTrigger(root).click();
  await expect(root.getByRole('heading', { name: 'Document Settings' })).toBeVisible();
};

const updateExternalId = async (surface: TEnvelopeEditorSurface, externalId: string) => {
  await openSettingsDialog(surface.root);
  await surface.root.locator('input[name="externalId"]').fill(externalId);
  await surface.root.getByRole('button', { name: 'Update' }).click();

  if (!surface.isEmbedded) {
    await expectToastTextToBeVisible(surface.root, 'Envelope updated');
    await surface.root.getByTestId('toast-close').click();
  }
};

const setupRecipientsForFieldPlacement = async (surface: TEnvelopeEditorSurface) => {
  if (surface.isEmbedded) {
    await expect(surface.root.getByRole('button', { name: 'Add Myself' })).toHaveCount(0);
    await setRecipientEmail(surface.root, 0, TEST_FIELD_VALUES.embeddedRecipient.email);
    await setRecipientName(surface.root, 0, TEST_FIELD_VALUES.embeddedRecipient.name);

    return TEST_FIELD_VALUES.embeddedRecipient.email;
  }

  await expect(surface.root.getByRole('button', { name: 'Add Myself' })).toBeVisible();
  await clickAddMyselfButton(surface.root);
  await expect(getRecipientEmailInputs(surface.root).first()).toHaveValue(surface.userEmail);

  return surface.userEmail;
};

type FieldButtonName =
  | 'Signature'
  | 'Email'
  | 'Name'
  | 'Initials'
  | 'Date'
  | 'Text'
  | 'Number'
  | 'Radio'
  | 'Checkbox'
  | 'Dropdown';

const placeFieldOnPdf = async (root: Page, fieldName: FieldButtonName, 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 });
};

const selectRecipientInFieldsStep = async (root: Page, recipientIdentifier: string) => {
  await root.locator('button[role="combobox"]').click();
  await root.getByText(recipientIdentifier).click();
};

const selectFieldOnCanvas = async (root: Page, position: { x: number; y: number }) => {
  const canvas = root.locator('.konva-container canvas').first();
  await expect(canvas).toBeVisible();
  await root.waitForTimeout(300);
  // Use force:true to bypass any floating action toolbar buttons that may intercept clicks.
  await canvas.click({ position, force: true });
};

/**
 * Shift+click a field on the canvas to toggle it in/out of the current multi-selection.
 */
const shiftClickFieldOnCanvas = async (root: Page, position: { x: number; y: number }) => {
  const canvas = root.locator('.konva-container canvas').first();
  await expect(canvas).toBeVisible();
  await root.waitForTimeout(300);
  // Use force:true to bypass any floating action toolbar buttons that may intercept clicks.
  await canvas.click({ position, modifiers: ['Shift'], force: true });
};

const runAddAndPersistSignatureTextFields = async (surface: TEnvelopeEditorSurface): Promise<TFieldFlowResult> => {
  const externalId = `e2e-fields-${nanoid()}`;

  if (surface.isEmbedded && !surface.envelopeId) {
    await addEnvelopeItemPdf(surface.root, 'embedded-fields.pdf');
  }

  await updateExternalId(surface, externalId);
  const recipientEmail = await setupRecipientsForFieldPlacement(surface);

  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 });
  let fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
  expect(fieldCount).toBe(1);

  await placeFieldOnPdf(surface.root, 'Text', { x: 220, y: 240 });
  fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  await clickEnvelopeEditorStep(surface.root, 'upload');
  await expect(surface.root.getByRole('heading', { name: 'Recipients' })).toBeVisible();

  await clickEnvelopeEditorStep(surface.root, 'addFields');
  await surface.root.locator('.konva-container canvas').first().waitFor({ state: 'visible' });
  await expect(surface.root.getByText('Selected Recipient')).toBeVisible();
  fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  return {
    externalId,
    recipientEmail,
  };
};

const getFieldMetaType = (fieldMeta: unknown) => {
  if (!isRecord(fieldMeta)) {
    return null;
  }

  return typeof fieldMeta.type === 'string' ? fieldMeta.type : null;
};

const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;

const assertFieldsPersistedInDatabase = async ({
  surface,
  externalId,
  recipientEmail,
}: {
  surface: TEnvelopeEditorSurface;
  externalId: string;
  recipientEmail: string;
}) => {
  const envelope = await prisma.envelope.findFirstOrThrow({
    where: {
      externalId,
      userId: surface.userId,
      teamId: surface.teamId,
      type: surface.envelopeType,
    },
    orderBy: {
      createdAt: 'desc',
    },
    include: {
      fields: true,
      recipients: true,
    },
  });

  const recipient = envelope.recipients.find((currentRecipient) => currentRecipient.email === recipientEmail);

  expect(recipient).toBeDefined();

  const fieldTypes = envelope.fields.map((field) => field.type).sort();
  const expectedFieldTypes = [FieldType.SIGNATURE, FieldType.TEXT].sort();

  expect(envelope.fields).toHaveLength(2);
  expect(fieldTypes).toEqual(expectedFieldTypes);
  expect(new Set(envelope.fields.map((field) => field.envelopeItemId)).size).toBe(1);
  expect(envelope.fields.every((field) => field.recipientId === recipient?.id)).toBe(true);

  const signatureField = envelope.fields.find((field) => field.type === FieldType.SIGNATURE);
  const textField = envelope.fields.find((field) => field.type === FieldType.TEXT);

  expect(getFieldMetaType(signatureField?.fieldMeta)).toBe('signature');
  expect(getFieldMetaType(textField?.fieldMeta)).toBe('text');
};

// --- Multi-recipient field flow ---

type TMultiRecipientFlowResult = {
  externalId: string;
  firstRecipientEmail: string;
  secondRecipientEmail: string;
};

const MULTI_RECIPIENT_VALUES = {
  secondSigner: {
    email: 'second-signer@test.documenso.com',
    name: 'Second Signer',
  },
};

const runMultiRecipientFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TMultiRecipientFlowResult> => {
  const externalId = `e2e-multi-recip-${nanoid()}`;
  const root = surface.root;

  if (surface.isEmbedded && !surface.envelopeId) {
    await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
  }

  await updateExternalId(surface, externalId);

  // Add two recipients.
  let firstRecipientEmail: string;

  if (surface.isEmbedded) {
    await setRecipientEmail(root, 0, TEST_FIELD_VALUES.embeddedRecipient.email);
    await setRecipientName(root, 0, TEST_FIELD_VALUES.embeddedRecipient.name);
    firstRecipientEmail = TEST_FIELD_VALUES.embeddedRecipient.email;
  } else {
    await clickAddMyselfButton(root);
    firstRecipientEmail = surface.userEmail;
  }

  await clickAddSignerButton(root);
  await setRecipientEmail(root, 1, MULTI_RECIPIENT_VALUES.secondSigner.email);
  await setRecipientName(root, 1, MULTI_RECIPIENT_VALUES.secondSigner.name);

  // Navigate to fields step.
  await clickEnvelopeEditorStep(root, 'addFields');
  await expect(root.getByText('Selected Recipient')).toBeVisible();
  await expect(root.locator('.konva-container canvas').first()).toBeVisible();

  let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(0);

  // Place Signature for recipient #1 (auto-selected).
  await placeFieldOnPdf(root, 'Signature', { x: 120, y: 140 });
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(1);

  // Switch recipient and place text field for recipient #2.
  await selectRecipientInFieldsStep(root, MULTI_RECIPIENT_VALUES.secondSigner.email);
  await placeFieldOnPdf(root, 'Text', { x: 220, y: 240 });
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  // Navigate away and back to ensure fields are persisted in the UI.
  await clickEnvelopeEditorStep(root, 'upload');
  await clickEnvelopeEditorStep(root, 'addFields');
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  // Phase 2: cascade deletion — go back to recipients and remove the second one.
  await clickEnvelopeEditorStep(root, 'upload');
  await expect(getRecipientEmailInputs(root)).toHaveCount(2);

  await getRecipientRemoveButtons(root).nth(1).click();
  await expect(getRecipientEmailInputs(root)).toHaveCount(1);

  // Go back to fields and verify cascade removal.
  await clickEnvelopeEditorStep(root, 'addFields');
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(1);

  return {
    externalId,
    firstRecipientEmail,
    secondRecipientEmail: MULTI_RECIPIENT_VALUES.secondSigner.email,
  };
};

const assertMultiRecipientCascadePersistedInDatabase = async ({
  surface,
  externalId,
  firstRecipientEmail,
}: {
  surface: TEnvelopeEditorSurface;
  externalId: string;
  firstRecipientEmail: string;
  secondRecipientEmail: string;
}) => {
  const envelope = await prisma.envelope.findFirstOrThrow({
    where: {
      externalId,
      userId: surface.userId,
      teamId: surface.teamId,
      type: surface.envelopeType,
    },
    orderBy: { createdAt: 'desc' },
    include: { fields: true, recipients: true },
  });

  // After cascade deletion, only one recipient and one field should remain.
  expect(envelope.recipients).toHaveLength(1);
  expect(envelope.recipients[0].email).toBe(firstRecipientEmail);

  expect(envelope.fields).toHaveLength(1);
  expect(envelope.fields[0].type).toBe(FieldType.SIGNATURE);
  expect(envelope.fields[0].recipientId).toBe(envelope.recipients[0].id);
};

// --- All 10 field types flow ---

type TAllFieldTypesFlowResult = {
  externalId: string;
};

const runAllFieldTypesFlow = async (surface: TEnvelopeEditorSurface): Promise<TAllFieldTypesFlowResult> => {
  const externalId = `e2e-all-fields-${nanoid()}`;
  const root = surface.root;

  if (surface.isEmbedded && !surface.envelopeId) {
    await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
  }

  await updateExternalId(surface, externalId);
  await setupRecipientsForFieldPlacement(surface);

  await clickEnvelopeEditorStep(root, 'addFields');
  await expect(root.locator('.konva-container canvas').first()).toBeVisible();

  // Place and configure each field type immediately after placement.
  // After placeFieldOnPdf, the sidebar shows the field's config form (field is selected in React state).

  // 1. Signature: place and set fontSize to 24.
  await placeFieldOnPdf(root, 'Signature', { x: 120, y: 50 });
  await root.locator('[data-testid="field-form-fontSize"]').fill('24');

  // 2. Email: place and set textAlign to center.
  await placeFieldOnPdf(root, 'Email', { x: 120, y: 100 });
  await root.locator('[data-testid="field-form-textAlign"]').click();
  await root.getByRole('option', { name: 'Center' }).click();

  // 3. Name: place and set textAlign to right.
  await placeFieldOnPdf(root, 'Name', { x: 120, y: 150 });
  await root.locator('[data-testid="field-form-textAlign"]').click();
  await root.getByRole('option', { name: 'Right' }).click();

  // 4. Initials: place and set fontSize to 16.
  await placeFieldOnPdf(root, 'Initials', { x: 120, y: 200 });
  await root.locator('[data-testid="field-form-fontSize"]').fill('16');

  // 5. Date: place and set textAlign to center.
  await placeFieldOnPdf(root, 'Date', { x: 120, y: 250 });
  await root.locator('[data-testid="field-form-textAlign"]').click();
  await root.getByRole('option', { name: 'Center' }).click();

  // 6. Text: place and configure label, placeholder, text, characterLimit, required.
  await placeFieldOnPdf(root, 'Text', { x: 120, y: 300 });
  await root.locator('[data-testid="field-form-label"]').fill('Test Label');
  await root.locator('[data-testid="field-form-placeholder"]').fill('Enter text here');
  await root.locator('[data-testid="field-form-text"]').fill('Default text value');
  await root.locator('[data-testid="field-form-characterLimit"]').fill('100');
  await root.locator('[data-testid="field-form-required"]').click();

  // 7. Number: place and configure label, placeholder, numberFormat, minValue, maxValue, required.
  await placeFieldOnPdf(root, 'Number', { x: 120, y: 350 });
  await root.locator('[data-testid="field-form-label"]').fill('Amount');
  await root.locator('[data-testid="field-form-placeholder"]').fill('0.00');
  await root.locator('[data-testid="field-form-numberFormat"]').click();
  await root.getByRole('option', { name: '123,456,789.00' }).click();
  await root.locator('[data-testid="field-form-minValue"]').fill('0');
  await root.locator('[data-testid="field-form-maxValue"]').fill('1000');
  await root.locator('[data-testid="field-form-required"]').click();

  // 8. Radio: place and configure two options, pre-select first, set direction to horizontal.
  await placeFieldOnPdf(root, 'Radio', { x: 120, y: 400 });

  // The first option already exists with default value "Default value". Fill it.
  await root.locator('[data-testid="field-form-values-0-value"]').fill('Option A');

  // Add a second option.
  await root.locator('[data-testid="field-form-values-add"]').click();
  await root.locator('[data-testid="field-form-values-1-value"]').fill('Option B');

  // Pre-select the first option (click its checkbox).
  await root.locator('[data-testid="field-form-values-0-checked"]').click();

  // Set direction to horizontal.
  await root.locator('[data-testid="field-form-direction"]').click();
  await root.getByRole('option', { name: 'Horizontal' }).click();

  // 9. Checkbox: place and configure two options, check both, set validation rule.
  await placeFieldOnPdf(root, 'Checkbox', { x: 120, y: 450 });

  // Fill first option value.
  await root.locator('[data-testid="field-form-values-0-value"]').fill('Check A');

  // Add a second option.
  await root.locator('[data-testid="field-form-values-add"]').click();
  await root.locator('[data-testid="field-form-values-1-value"]').fill('Check B');

  // Check both options (click their checkboxes).
  await root.locator('[data-testid="field-form-values-0-checked"]').click();
  await root.locator('[data-testid="field-form-values-1-checked"]').click();

  // Set validation: "Select at least" 1.
  await root.locator('[data-testid="field-form-validationRule"]').click();
  await root.getByRole('option', { name: 'Select at least' }).click();

  // Set validation length to 1.
  await root.locator('[data-testid="field-form-validationLength"]').click();
  await root.getByRole('option', { name: '1', exact: true }).click();

  // 10. Dropdown: place and configure two options, set default value.
  await placeFieldOnPdf(root, 'Dropdown', { x: 120, y: 500 });

  // First option already has "Option 1". Change it to "Red".
  await root.locator('[data-testid="field-form-values-0-value"]').fill('Red');

  // Add a second option.
  await root.locator('[data-testid="field-form-values-add"]').click();
  await root.locator('[data-testid="field-form-values-1-value"]').clear();
  await root.locator('[data-testid="field-form-values-1-value"]').fill('Blue');

  // Set default value to "Red".
  await root.locator('[data-testid="field-form-defaultValue"]').click();
  await root.getByRole('option', { name: 'Red' }).click();

  let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(10);

  // Wait briefly for auto-save to fire on the last configured field.
  await root.waitForTimeout(500);

  // Navigate away and back to verify persistence.
  await clickEnvelopeEditorStep(root, 'upload');
  await clickEnvelopeEditorStep(root, 'addFields');
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(10);

  return { externalId };
};

const assertAllFieldTypesPersistedInDatabase = async ({
  surface,
  externalId,
}: {
  surface: TEnvelopeEditorSurface;
  externalId: string;
}) => {
  const envelope = await prisma.envelope.findFirstOrThrow({
    where: {
      externalId,
      userId: surface.userId,
      teamId: surface.teamId,
      type: surface.envelopeType,
    },
    orderBy: { createdAt: 'desc' },
    include: { fields: true },
  });

  expect(envelope.fields).toHaveLength(10);

  const fieldsByType = new Map(envelope.fields.map((f) => [f.type, f]));

  // Helper to safely access fieldMeta as a record.
  const meta = (type: FieldType): Record<string, unknown> => {
    const field = fieldsByType.get(type);
    expect(field).toBeDefined();
    const fieldMeta = field!.fieldMeta;
    expect(typeof fieldMeta).toBe('object');
    expect(fieldMeta).not.toBeNull();
    return fieldMeta as Record<string, unknown>;
  };

  // SIGNATURE
  expect(meta(FieldType.SIGNATURE).type).toBe('signature');
  expect(meta(FieldType.SIGNATURE).fontSize).toBe(24);

  // EMAIL
  expect(meta(FieldType.EMAIL).type).toBe('email');
  expect(meta(FieldType.EMAIL).textAlign).toBe('center');

  // NAME
  expect(meta(FieldType.NAME).type).toBe('name');
  expect(meta(FieldType.NAME).textAlign).toBe('right');

  // INITIALS
  expect(meta(FieldType.INITIALS).type).toBe('initials');
  expect(meta(FieldType.INITIALS).fontSize).toBe(16);

  // DATE
  expect(meta(FieldType.DATE).type).toBe('date');
  expect(meta(FieldType.DATE).textAlign).toBe('center');

  // TEXT
  expect(meta(FieldType.TEXT).type).toBe('text');
  expect(meta(FieldType.TEXT).label).toBe('Test Label');
  expect(meta(FieldType.TEXT).placeholder).toBe('Enter text here');
  expect(meta(FieldType.TEXT).text).toBe('Default text value');
  expect(meta(FieldType.TEXT).characterLimit).toBe(100);
  expect(meta(FieldType.TEXT).required).toBe(true);

  // NUMBER
  expect(meta(FieldType.NUMBER).type).toBe('number');
  expect(meta(FieldType.NUMBER).label).toBe('Amount');
  expect(meta(FieldType.NUMBER).placeholder).toBe('0.00');
  expect(meta(FieldType.NUMBER).numberFormat).toBe('123,456,789.00');
  expect(meta(FieldType.NUMBER).minValue).toBe(0);
  expect(meta(FieldType.NUMBER).maxValue).toBe(1000);
  expect(meta(FieldType.NUMBER).required).toBe(true);

  // RADIO
  expect(meta(FieldType.RADIO).type).toBe('radio');
  expect(meta(FieldType.RADIO).direction).toBe('horizontal');
  const radioValues = meta(FieldType.RADIO).values as Array<{
    value: string;
    checked: boolean;
  }>;
  expect(radioValues).toHaveLength(2);
  expect(radioValues[0].value).toBe('Option A');
  expect(radioValues[0].checked).toBe(true);
  expect(radioValues[1].value).toBe('Option B');
  expect(radioValues[1].checked).toBe(false);

  // CHECKBOX
  expect(meta(FieldType.CHECKBOX).type).toBe('checkbox');
  expect(meta(FieldType.CHECKBOX).validationRule).toBe('Select at least');
  expect(meta(FieldType.CHECKBOX).validationLength).toBe(1);
  const checkboxValues = meta(FieldType.CHECKBOX).values as Array<{
    value: string;
    checked: boolean;
  }>;
  expect(checkboxValues).toHaveLength(2);
  expect(checkboxValues[0].value).toBe('Check A');
  expect(checkboxValues[0].checked).toBe(true);
  expect(checkboxValues[1].value).toBe('Check B');
  expect(checkboxValues[1].checked).toBe(true);

  // DROPDOWN
  expect(meta(FieldType.DROPDOWN).type).toBe('dropdown');
  expect(meta(FieldType.DROPDOWN).defaultValue).toBe('Red');
  const dropdownValues = meta(FieldType.DROPDOWN).values as Array<{ value: string }>;
  expect(dropdownValues).toHaveLength(2);
  expect(dropdownValues[0].value).toBe('Red');
  expect(dropdownValues[1].value).toBe('Blue');
};

// --- Duplicate and delete fields flow ---

type TDuplicateDeleteFlowResult = {
  externalId: string;
};

const runDuplicateDeleteFieldFlow = async (surface: TEnvelopeEditorSurface): Promise<TDuplicateDeleteFlowResult> => {
  const externalId = `e2e-dup-del-${nanoid()}`;
  const root = surface.root;

  if (surface.isEmbedded && !surface.envelopeId) {
    await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
  }

  await updateExternalId(surface, externalId);
  await setupRecipientsForFieldPlacement(surface);

  await clickEnvelopeEditorStep(root, 'addFields');
  await expect(root.locator('.konva-container canvas').first()).toBeVisible();

  // Place a Signature field.
  await placeFieldOnPdf(root, 'Signature', { x: 150, y: 150 });
  let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(1);

  // Select the field on canvas to show the action toolbar.
  await selectFieldOnCanvas(root, { x: 150, y: 150 });
  await expect(root.locator('button[title="Duplicate"]')).toBeVisible();

  // Duplicate the field.
  await root.locator('button[title="Duplicate"]').click();
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  // Navigate away and back to persist changes.
  await clickEnvelopeEditorStep(root, 'upload');
  await clickEnvelopeEditorStep(root, 'addFields');
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  // Select a field and delete it via the Remove button.
  await selectFieldOnCanvas(root, { x: 150, y: 150 });
  await expect(root.locator('button[title="Remove"]')).toBeVisible();
  await root.locator('button[title="Remove"]').click();
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(1);

  // Navigate away and back to verify persistence.
  await clickEnvelopeEditorStep(root, 'upload');
  await clickEnvelopeEditorStep(root, 'addFields');
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(1);

  return { externalId };
};

const assertDuplicateDeleteFieldPersistedInDatabase = async ({
  surface,
  externalId,
}: {
  surface: TEnvelopeEditorSurface;
  externalId: string;
}) => {
  const envelope = await prisma.envelope.findFirstOrThrow({
    where: {
      externalId,
      userId: surface.userId,
      teamId: surface.teamId,
      type: surface.envelopeType,
    },
    orderBy: { createdAt: 'desc' },
    include: { fields: true },
  });

  // After duplicating (2 fields) then deleting one, exactly 1 SIGNATURE field should remain.
  expect(envelope.fields).toHaveLength(1);
  expect(envelope.fields[0].type).toBe(FieldType.SIGNATURE);
};

// --- Change field type flow ---

type TChangeFieldTypeFlowResult = {
  externalId: string;
};

const FIELD_A_POSITION = { x: 150, y: 150 };
const FIELD_B_POSITION = { x: 150, y: 250 };

const changeFieldTypeViaToolbar = async (root: Page, newTypeLabel: FieldButtonName) => {
  await expect(root.locator('button[title="Change Field Type"]')).toBeVisible();
  await root.locator('button[title="Change Field Type"]').click();

  // The CommandDialog uses role="option" for items; sidebar palette buttons use role="button".
  const option = root.getByRole('option', { name: newTypeLabel, exact: true });
  await expect(option).toBeVisible();
  await option.click();

  // Wait for the CommandDialog to close (selection persists so the toolbar remains).
  await expect(root.getByRole('dialog')).toHaveCount(0);
};

/**
 * Multi-select fields on the konva canvas by drawing a marquee selection rectangle.
 *
 * The editor's stage mousedown/mousemove/mouseup handlers create a Konva selection
 * rectangle when the user drags on empty stage area. All field groups that intersect
 * the rectangle are selected at once. This is the canonical multi-select gesture.
 */
const marqueeSelectFieldsOnCanvas = async (
  root: Page,
  start: { x: number; y: number },
  end: { x: number; y: number },
) => {
  const canvas = root.locator('.konva-container canvas').first();
  await expect(canvas).toBeVisible();

  const box = await canvas.boundingBox();

  if (!box) {
    throw new Error('Canvas bounding box not available for marquee selection.');
  }

  // The marquee gesture must start on empty stage (not on a field) and pass through
  // intermediate points so the editor's mousemove handler can grow the rectangle.
  await root.mouse.move(box.x + start.x, box.y + start.y);
  await root.mouse.down();
  await root.mouse.move(box.x + (start.x + end.x) / 2, box.y + (start.y + end.y) / 2, { steps: 5 });
  await root.mouse.move(box.x + end.x, box.y + end.y, { steps: 5 });
  await root.mouse.up();
};

const runChangeFieldTypeFlow = async (surface: TEnvelopeEditorSurface): Promise<TChangeFieldTypeFlowResult> => {
  const externalId = `e2e-change-type-${nanoid()}`;
  const root = surface.root;

  if (surface.isEmbedded && !surface.envelopeId) {
    await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
  }

  await updateExternalId(surface, externalId);
  await setupRecipientsForFieldPlacement(surface);

  await clickEnvelopeEditorStep(root, 'addFields');
  await expect(root.locator('.konva-container canvas').first()).toBeVisible();

  // Place two fields of different types: Signature (A) and Name (B).
  await placeFieldOnPdf(root, 'Signature', FIELD_A_POSITION);
  await placeFieldOnPdf(root, 'Name', FIELD_B_POSITION);
  let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  // --- Phase 1: single field type change ---
  // Select field A (Signature) and change it to Text via the toolbar.
  await selectFieldOnCanvas(root, FIELD_A_POSITION);
  await changeFieldTypeViaToolbar(root, 'Text');

  // Field count must remain stable -- changing type doesn't add/remove fields.
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  // Navigate away and back to verify the change is persisted in local state.
  await clickEnvelopeEditorStep(root, 'upload');
  await clickEnvelopeEditorStep(root, 'addFields');
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  // --- Phase 2: multi-field type change ---
  // Use a marquee drag-selection rectangle to capture both fields at once.
  // Fields are at (150, 150) and (150, 250) with default dims ~90x30; drag from
  // (50, 100) to (260, 290) encloses both with margin.
  await marqueeSelectFieldsOnCanvas(root, { x: 50, y: 100 }, { x: 260, y: 290 });

  // With mixed-type selection (Text + Name), change both to Date.
  await changeFieldTypeViaToolbar(root, 'Date');

  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  // Navigate away and back to verify persistence.
  await clickEnvelopeEditorStep(root, 'upload');
  await clickEnvelopeEditorStep(root, 'addFields');
  fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
  expect(fieldCount).toBe(2);

  return { externalId };
};

const assertChangeFieldTypePersistedInDatabase = async ({
  surface,
  externalId,
}: {
  surface: TEnvelopeEditorSurface;
  externalId: string;
}) => {
  const envelope = await prisma.envelope.findFirstOrThrow({
    where: {
      externalId,
      userId: surface.userId,
      teamId: surface.teamId,
      type: surface.envelopeType,
    },
    orderBy: { createdAt: 'desc' },
    include: { fields: true },
  });

  // Started with Signature + Name, then both were converted to Date.
  // Use sorted .map() in the assertion so any failure prints which types were found.
  const actualTypes = envelope.fields.map((field) => field.type).sort();
  const expectedTypes = [FieldType.DATE, FieldType.DATE];

  expect(envelope.fields).toHaveLength(2);
  expect(actualTypes).toEqual(expectedTypes);

  // Each field's meta must have been reset to the new type's defaults.
  const actualMetaTypes = envelope.fields.map((field) => getFieldMetaType(field.fieldMeta)).sort();
  expect(actualMetaTypes).toEqual(['date', 'date']);
};

// --- Shift+click multi-select flow ---

type TShiftClickFlowResult = {
  externalId: string;
};

const SHIFT_CLICK_FIELD_POSITIONS = {
  signature: { x: 150, y: 120 },
  text: { x: 150, y: 260 },
  name: { x: 150, y: 400 },
};

const runShiftClickMultiSelectFlow = async (surface: TEnvelopeEditorSurface): Promise<TShiftClickFlowResult> => {
  const externalId = `e2e-shift-click-${nanoid()}`;
  const root = surface.root;

  if (surface.isEmbedded && !surface.envelopeId) {
    await addEnvelopeItemPdf(root, 'embedded-fields.pdf');
  }

  await updateExternalId(surface, externalId);
  await setupRecipientsForFieldPlacement(surface);

  await clickEnvelopeEditorStep(root, 'addFields');
  await expect(root.locator('.konva-container canvas').first()).toBeVisible();

  // Place three fields, spaced far enough apart that their action toolbars don't
  // overlap a neighbouring field's click target.
  await placeFieldOnPdf(root, 'Signature', SHIFT_CLICK_FIELD_POSITIONS.signature);
  await placeFieldOnPdf(root, 'Text', SHIFT_CLICK_FIELD_POSITIONS.text);
  await placeFieldOnPdf(root, 'Name', SHIFT_CLICK_FIELD_POSITIONS.name);
  expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(3);

  // A plain click selects exactly one field.
  await selectFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature);
  await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(1);

  // Shift+click a second field ADDS it to the selection (the new behaviour).
  await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.text);
  await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(2);

  // Shift+click an already-selected field REMOVES it from the selection.
  await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature);
  await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(1);

  // Shift+click it again RE-ADDS it, leaving Signature + Text selected and Name excluded.
  await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature);
  await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(2);

  // Delete the two selected fields via the floating action toolbar. Only the
  // un-selected Name field should remain -- proving the multi-selection contained
  // exactly the two Shift-clicked fields.
  await expect(root.locator('button[title="Remove"]')).toBeVisible();
  await root.locator('button[title="Remove"]').click();
  expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(1);

  // Navigate away and back to verify persistence.
  await clickEnvelopeEditorStep(root, 'upload');
  await clickEnvelopeEditorStep(root, 'addFields');
  expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(1);

  return { externalId };
};

const assertShiftClickMultiSelectPersistedInDatabase = async ({
  surface,
  externalId,
}: {
  surface: TEnvelopeEditorSurface;
  externalId: string;
}) => {
  const envelope = await prisma.envelope.findFirstOrThrow({
    where: {
      externalId,
      userId: surface.userId,
      teamId: surface.teamId,
      type: surface.envelopeType,
    },
    orderBy: { createdAt: 'desc' },
    include: { fields: true },
  });

  // Signature + Text were multi-selected via Shift+click and deleted; only Name remains.
  expect(envelope.fields).toHaveLength(1);
  expect(envelope.fields[0].type).toBe(FieldType.NAME);
};

// --- Test describe blocks ---

test.describe('document editor', () => {
  test('shift+click adds and removes fields from the selection', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);
    const result = await runShiftClickMultiSelectFlow(surface);

    await assertShiftClickMultiSelectPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('add and persist signature/text fields', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);
    const result = await runAddAndPersistSignatureTextFields(surface);

    await assertFieldsPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('multi-recipient field placement, switching, and cascade deletion', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);
    const result = await runMultiRecipientFieldFlow(surface);

    await assertMultiRecipientCascadePersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('duplicate and delete fields via canvas action toolbar', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);
    const result = await runDuplicateDeleteFieldFlow(surface);

    await assertDuplicateDeleteFieldPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('place and configure all 10 field types', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);
    const result = await runAllFieldTypesFlow(surface);

    await assertAllFieldTypesPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('change field type via canvas action toolbar (single and multi-select)', async ({ page }) => {
    const surface = await openDocumentEnvelopeEditor(page);
    const result = await runChangeFieldTypeFlow(surface);

    await assertChangeFieldTypePersistedInDatabase({
      surface,
      ...result,
    });
  });
});

test.describe('template editor', () => {
  test('shift+click adds and removes fields from the selection', async ({ page }) => {
    const surface = await openTemplateEnvelopeEditor(page);
    const result = await runShiftClickMultiSelectFlow(surface);

    await assertShiftClickMultiSelectPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('add and persist signature/text fields', async ({ page }) => {
    const surface = await openTemplateEnvelopeEditor(page);
    const result = await runAddAndPersistSignatureTextFields(surface);

    await assertFieldsPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('multi-recipient field placement, switching, and cascade deletion', async ({ page }) => {
    const surface = await openTemplateEnvelopeEditor(page);
    const result = await runMultiRecipientFieldFlow(surface);

    await assertMultiRecipientCascadePersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('duplicate and delete fields via canvas action toolbar', async ({ page }) => {
    const surface = await openTemplateEnvelopeEditor(page);
    const result = await runDuplicateDeleteFieldFlow(surface);

    await assertDuplicateDeleteFieldPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('place and configure all 10 field types', async ({ page }) => {
    const surface = await openTemplateEnvelopeEditor(page);
    const result = await runAllFieldTypesFlow(surface);

    await assertAllFieldTypesPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('change field type via canvas action toolbar (single and multi-select)', async ({ page }) => {
    const surface = await openTemplateEnvelopeEditor(page);
    const result = await runChangeFieldTypeFlow(surface);

    await assertChangeFieldTypePersistedInDatabase({
      surface,
      ...result,
    });
  });
});

test.describe('embedded create', () => {
  test('shift+click adds and removes fields from the selection', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'DOCUMENT',
      tokenNamePrefix: 'e2e-embed-shift-click',
    });
    const result = await runShiftClickMultiSelectFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertShiftClickMultiSelectPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('add and persist signature/text fields', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'DOCUMENT',
      tokenNamePrefix: 'e2e-embed-fields',
    });
    const result = await runAddAndPersistSignatureTextFields(surface);

    await persistEmbeddedEnvelope(surface);

    await assertFieldsPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('multi-recipient field placement, switching, and cascade deletion', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'DOCUMENT',
      tokenNamePrefix: 'e2e-embed-multi-recip',
    });
    const result = await runMultiRecipientFieldFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertMultiRecipientCascadePersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('duplicate and delete fields via canvas action toolbar', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'DOCUMENT',
      tokenNamePrefix: 'e2e-embed-dup-del',
    });
    const result = await runDuplicateDeleteFieldFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertDuplicateDeleteFieldPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('place and configure all 10 field types', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'DOCUMENT',
      tokenNamePrefix: 'e2e-embed-all-fields',
    });
    const result = await runAllFieldTypesFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertAllFieldTypesPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('change field type via canvas action toolbar (single and multi-select)', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'DOCUMENT',
      tokenNamePrefix: 'e2e-embed-change-type',
    });
    const result = await runChangeFieldTypeFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertChangeFieldTypePersistedInDatabase({
      surface,
      ...result,
    });
  });
});

test.describe('embedded edit', () => {
  test('shift+click adds and removes fields from the selection', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'TEMPLATE',
      mode: 'edit',
      tokenNamePrefix: 'e2e-embed-shift-click',
    });
    const result = await runShiftClickMultiSelectFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertShiftClickMultiSelectPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('add and persist signature/text fields', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'TEMPLATE',
      mode: 'edit',
      tokenNamePrefix: 'e2e-embed-fields',
    });
    const result = await runAddAndPersistSignatureTextFields(surface);

    await persistEmbeddedEnvelope(surface);

    await assertFieldsPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('multi-recipient field placement, switching, and cascade deletion', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'TEMPLATE',
      mode: 'edit',
      tokenNamePrefix: 'e2e-embed-multi-recip',
    });
    const result = await runMultiRecipientFieldFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertMultiRecipientCascadePersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('duplicate and delete fields via canvas action toolbar', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'TEMPLATE',
      mode: 'edit',
      tokenNamePrefix: 'e2e-embed-dup-del',
    });
    const result = await runDuplicateDeleteFieldFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertDuplicateDeleteFieldPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('place and configure all 10 field types', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'TEMPLATE',
      mode: 'edit',
      tokenNamePrefix: 'e2e-embed-all-fields',
    });
    const result = await runAllFieldTypesFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertAllFieldTypesPersistedInDatabase({
      surface,
      ...result,
    });
  });

  test('change field type via canvas action toolbar (single and multi-select)', async ({ page }) => {
    const surface = await openEmbeddedEnvelopeEditor(page, {
      envelopeType: 'TEMPLATE',
      mode: 'edit',
      tokenNamePrefix: 'e2e-embed-change-type',
    });
    const result = await runChangeFieldTypeFlow(surface);

    await persistEmbeddedEnvelope(surface);

    await assertChangeFieldTypePersistedInDatabase({
      surface,
      ...result,
    });
  });
});
