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 { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { prisma } from '@documenso/prisma';
import {
  DocumentDistributionMethod,
  DocumentSigningOrder,
  DocumentStatus,
  DocumentVisibility,
  EnvelopeType,
  FieldType,
  FolderType,
  RecipientRole,
} from '@documenso/prisma/client';
import { seedUser } from '@documenso/prisma/seed/users';
import type {
  TCreateEnvelopePayload,
  TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.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 { TUpdateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/update-envelope-recipients.types';
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
import { type APIRequestContext, expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import { pick } from 'remeda';

import { ALIGNMENT_TEST_FIELDS } from '../../../constants/field-alignment-pdf';
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';

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

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

test.describe('API V2 Envelopes', () => {
  let userA: User, teamA: Team, userB: User, teamB: Team, tokenA: string, tokenB: string;

  test.beforeEach(async () => {
    ({ user: userA, team: teamA } = await seedUser());
    ({ token: tokenA } = await createApiToken({
      userId: userA.id,
      teamId: teamA.id,
      tokenName: 'userA',
      expiresIn: null,
    }));

    ({ user: userB, team: teamB } = await seedUser());
    ({ token: tokenB } = await createApiToken({
      userId: userB.id,
      teamId: teamB.id,
      tokenName: 'userB',
      expiresIn: null,
    }));
  });

  test.describe('Envelope create endpoint', () => {
    test('should fail on invalid form', async ({ request }) => {
      const payload = {
        type: 'Invalid Type',
        title: 'Test Envelope',
      };

      const formData = new FormData();
      formData.append('payload', JSON.stringify(payload));

      const res = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenB}` },
        multipart: formData,
      });

      expect(res.ok()).toBeFalsy();
      expect(res.status()).toBe(400);
    });

    test('should create envelope with single file', async ({ request }) => {
      const payload = {
        type: EnvelopeType.TEMPLATE,
        title: 'Test Envelope',
      } satisfies TCreateEnvelopePayload;

      const formData = new FormData();

      formData.append('payload', JSON.stringify(payload));

      const files = [
        {
          name: 'field-font-alignment.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/field-font-alignment.pdf')),
        },
      ];

      for (const file of files) {
        formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
      }

      const res = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenB}` },
        multipart: formData,
      });

      expect(res.ok()).toBeTruthy();
      expect(res.status()).toBe(200);

      const response = (await res.json()) as TCreateEnvelopeResponse;

      const envelope = await prisma.envelope.findUnique({
        where: {
          id: response.id,
        },
        include: {
          envelopeItems: true,
        },
      });

      expect(envelope).toBeDefined();
      expect(envelope?.title).toBe('Test Envelope');
      expect(envelope?.type).toBe(EnvelopeType.TEMPLATE);
      expect(envelope?.status).toBe(DocumentStatus.DRAFT);
      expect(envelope?.envelopeItems.length).toBe(1);
      expect(envelope?.envelopeItems[0].title).toBe('field-font-alignment.pdf');
      expect(envelope?.envelopeItems[0].documentDataId).toBeDefined();
    });

    test('should create envelope with multiple file', async ({ request }) => {
      const folder = await prisma.folder.create({
        data: {
          name: 'Test Folder',
          teamId: teamA.id,
          userId: userA.id,
          type: FolderType.DOCUMENT,
        },
      });

      const payload = {
        title: 'Envelope Title',
        type: EnvelopeType.DOCUMENT,
        externalId: 'externalId',
        visibility: DocumentVisibility.MANAGER_AND_ABOVE,
        globalAccessAuth: ['ACCOUNT'],
        // Ignore this error in the test since it doesn't actually exist in the PDF:
        // - Error setting value for field hello: PDFDocument has no form field with the name "hello"
        // We want to check if the form value is set in the DB.
        formValues: {
          hello: 'world',
        },
        folderId: folder.id,
        recipients: [
          {
            email: userA.email,
            name: 'Name',
            role: RecipientRole.SIGNER,
            accessAuth: ['TWO_FACTOR_AUTH'],
            signingOrder: 1,
            fields: [
              {
                type: FieldType.SIGNATURE,
                identifier: 'field-font-alignment.pdf',
                page: 1,
                positionX: 0,
                positionY: 0,
                width: 0,
                height: 0,
                fieldMeta: { type: 'signature' },
              },
              {
                type: FieldType.SIGNATURE,
                identifier: 0,
                page: 1,
                positionX: 0,
                positionY: 0,
                width: 0,
                height: 0,
                fieldMeta: { type: 'signature' },
              },
            ],
          },
        ],
        meta: {
          subject: 'Subject',
          message: 'Message',
          timezone: 'Europe/Berlin',
          dateFormat: 'dd.MM.yyyy',
          distributionMethod: DocumentDistributionMethod.NONE,
          signingOrder: DocumentSigningOrder.SEQUENTIAL,
          allowDictateNextSigner: true,
          redirectUrl: 'https://documenso.com',
          language: 'de',
          typedSignatureEnabled: true,
          uploadSignatureEnabled: false,
          drawSignatureEnabled: false,
          emailReplyTo: userA.email,
          emailSettings: {
            recipientSigningRequest: false,
            recipientRemoved: false,
            recipientSigned: false,
            documentPending: false,
            documentCompleted: false,
            documentDeleted: false,
            ownerRecipientExpired: true,
            ownerDocumentCompleted: true,
            ownerDocumentCreated: true,
          },
        },
        attachments: [
          {
            label: 'Test Attachment',
            data: 'https://documenso.com',
            type: 'link',
          },
        ],
      } satisfies TCreateEnvelopePayload;

      const formData = new FormData();

      formData.append('payload', JSON.stringify(payload));

      const files = [
        {
          name: 'field-meta.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/field-meta.pdf')),
        },
        {
          name: 'field-font-alignment.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/field-font-alignment.pdf')),
        },
      ];

      for (const file of files) {
        formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
      }

      // Should error since folder is not owned by the user.
      const invalidRes = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenB}` },
        multipart: formData,
      });

      expect(invalidRes.ok()).toBeFalsy();

      const res = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        multipart: formData,
      });

      expect(res.ok()).toBeTruthy();
      expect(res.status()).toBe(200);

      const response = (await res.json()) as TCreateEnvelopeResponse;

      const envelope = await prisma.envelope.findUniqueOrThrow({
        where: {
          id: response.id,
        },
        include: {
          documentMeta: true,
          envelopeItems: true,
          recipients: true,
          fields: true,
          envelopeAttachments: true,
        },
      });

      expect(envelope.envelopeItems.length).toBe(2);
      expect(envelope.envelopeItems[0].title).toBe('field-meta.pdf');
      expect(envelope.envelopeItems[1].title).toBe('field-font-alignment.pdf');

      expect(envelope.title).toBe(payload.title);
      expect(envelope.type).toBe(payload.type);
      expect(envelope.externalId).toBe(payload.externalId);
      expect(envelope.visibility).toBe(payload.visibility);
      expect(envelope.authOptions).toEqual({
        globalAccessAuth: payload.globalAccessAuth,
        globalActionAuth: [],
      });
      expect(envelope.formValues).toEqual(payload.formValues);
      expect(envelope.folderId).toBe(payload.folderId);

      expect(envelope.documentMeta.subject).toBe(payload.meta.subject);
      expect(envelope.documentMeta.message).toBe(payload.meta.message);
      expect(envelope.documentMeta.timezone).toBe(payload.meta.timezone);
      expect(envelope.documentMeta.dateFormat).toBe(payload.meta.dateFormat);
      expect(envelope.documentMeta.distributionMethod).toBe(payload.meta.distributionMethod);
      expect(envelope.documentMeta.signingOrder).toBe(payload.meta.signingOrder);
      expect(envelope.documentMeta.allowDictateNextSigner).toBe(payload.meta.allowDictateNextSigner);
      expect(envelope.documentMeta.redirectUrl).toBe(payload.meta.redirectUrl);
      expect(envelope.documentMeta.language).toBe(payload.meta.language);
      expect(envelope.documentMeta.typedSignatureEnabled).toBe(payload.meta.typedSignatureEnabled);
      expect(envelope.documentMeta.uploadSignatureEnabled).toBe(payload.meta.uploadSignatureEnabled);
      expect(envelope.documentMeta.drawSignatureEnabled).toBe(payload.meta.drawSignatureEnabled);
      expect(envelope.documentMeta.emailReplyTo).toBe(payload.meta.emailReplyTo);
      expect(envelope.documentMeta.emailSettings).toEqual(payload.meta.emailSettings);

      expect([
        {
          label: envelope.envelopeAttachments[0].label,
          data: envelope.envelopeAttachments[0].data,
          type: envelope.envelopeAttachments[0].type,
        },
      ]).toEqual(payload.attachments);

      const field = envelope.fields[0];
      const recipient = envelope.recipients[0];

      expect({
        email: recipient.email,
        name: recipient.name,
        role: recipient.role,
        signingOrder: recipient.signingOrder,
        accessAuth: recipient.authOptions?.accessAuth,
      }).toEqual(pick(payload.recipients[0], ['email', 'name', 'role', 'signingOrder', 'accessAuth']));

      expect({
        type: field.type,
        page: field.page,
        positionX: field.positionX.toNumber(),
        positionY: field.positionY.toNumber(),
        width: field.width.toNumber(),
        height: field.height.toNumber(),
      }).toEqual(pick(payload.recipients[0].fields[0], ['type', 'page', 'positionX', 'positionY', 'width', 'height']));

      // Expect string based ID to work.
      expect(field.envelopeItemId).toBe(
        envelope.envelopeItems.find((item) => item.title === 'field-font-alignment.pdf')?.id,
      );

      // Expect index based ID to work.
      expect(envelope.fields[1].envelopeItemId).toBe(
        envelope.envelopeItems.find((item) => item.title === 'field-meta.pdf')?.id,
      );
    });
  });

  /**
   * Creates envelopes with the two field test PDFs.
   */
  test('Envelope full test', async ({ request }) => {
    // Step 1: Create initial envelope with Prisma (with first envelope item)
    const alignmentPdf = fs.readFileSync(path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'));

    const fieldMetaPdf = fs.readFileSync(path.join(__dirname, '../../../../../assets/field-meta.pdf'));

    const formData = new FormData();

    formData.append(
      'payload',
      JSON.stringify({
        type: EnvelopeType.DOCUMENT,
        title: 'Envelope Full Field Test',
      } satisfies TCreateEnvelopePayload),
    );

    // Only add one file for now.
    formData.append('files', new File([alignmentPdf], 'field-font-alignment.pdf', { type: 'application/pdf' }));

    const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
      headers: { Authorization: `Bearer ${tokenA}` },
      multipart: formData,
    });

    expect(createEnvelopeRequest.ok()).toBeTruthy();
    expect(createEnvelopeRequest.status()).toBe(200);

    const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();

    const getEnvelopeRequest = await request.get(`${baseUrl}/envelope/${createdEnvelopeId}`, {
      headers: { Authorization: `Bearer ${tokenA}` },
    });

    const createdEnvelope: TGetEnvelopeResponse = await getEnvelopeRequest.json();

    // Might as well testing access control here as well.
    const unauthRequest = await request.get(`${baseUrl}/envelope/${createdEnvelopeId}`, {
      headers: { Authorization: `Bearer ${tokenB}` },
    });

    expect(unauthRequest.ok()).toBeFalsy();
    expect(unauthRequest.status()).toBe(404);

    // Step 2: Create second envelope item via API
    const createEnvelopeItemsPayload: TCreateEnvelopeItemsPayload = {
      envelopeId: createdEnvelope.id,
    };

    const createEnvelopeItemFormData = new FormData();
    createEnvelopeItemFormData.append('payload', JSON.stringify(createEnvelopeItemsPayload));
    createEnvelopeItemFormData.append('files', new File([fieldMetaPdf], 'field-meta.pdf', { type: 'application/pdf' }));

    const createItemsRes = await request.post(`${baseUrl}/envelope/item/create-many`, {
      headers: { Authorization: `Bearer ${tokenA}` },
      multipart: createEnvelopeItemFormData,
    });

    expect(createItemsRes.ok()).toBeTruthy();
    expect(createItemsRes.status()).toBe(200);

    // Step 3: Update envelope via API
    const updateEnvelopeRequest: TUpdateEnvelopeRequest = {
      envelopeId: createdEnvelope.id,
      data: {
        title: 'Envelope Full Field Test',
        visibility: DocumentVisibility.MANAGER_AND_ABOVE,
      },
    };

    const updateRes = await request.post(`${baseUrl}/envelope/update`, {
      headers: { Authorization: `Bearer ${tokenA}` },
      data: updateEnvelopeRequest,
    });

    expect(updateRes.ok()).toBeTruthy();
    expect(updateRes.status()).toBe(200);

    // Step 4: Create recipient via API
    const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
      envelopeId: createdEnvelope.id,
      data: [
        {
          email: userA.email,
          name: userA.name || '',
          role: RecipientRole.SIGNER,
          accessAuth: [],
          actionAuth: [],
        },
      ],
    };

    const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
      headers: { Authorization: `Bearer ${tokenA}` },
      data: createRecipientsRequest,
    });

    expect(createRecipientsRes.ok()).toBeTruthy();
    expect(createRecipientsRes.status()).toBe(200);

    // Step 5: Get envelope to retrieve recipients and envelope items
    const getRes = await request.get(`${baseUrl}/envelope/${createdEnvelope.id}`, {
      headers: { Authorization: `Bearer ${tokenA}` },
    });

    expect(getRes.ok()).toBeTruthy();
    expect(getRes.status()).toBe(200);

    const envelopeResponse = (await getRes.json()) as TGetEnvelopeResponse;

    const recipientId = envelopeResponse.recipients[0].id;
    const alignmentItem = envelopeResponse.envelopeItems.find((item: { order: number }) => item.order === 1);
    const fieldMetaItem = envelopeResponse.envelopeItems.find((item: { order: number }) => item.order === 2);

    expect(recipientId).toBeDefined();
    expect(alignmentItem).toBeDefined();
    expect(fieldMetaItem).toBeDefined();

    if (!alignmentItem || !fieldMetaItem) {
      throw new Error('Envelope items not found');
    }

    // Step 6: Create fields for first PDF (alignment fields)
    const alignmentFieldsRequest = {
      envelopeId: createdEnvelope.id,
      data: ALIGNMENT_TEST_FIELDS.map((field) => ({
        recipientId,
        envelopeItemId: alignmentItem.id,
        type: field.type,
        page: field.page,
        positionX: field.positionX,
        positionY: field.positionY,
        width: field.width,
        height: field.height,
        fieldMeta: field.fieldMeta,
      })),
    };

    const createAlignmentFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
      headers: { Authorization: `Bearer ${tokenA}` },
      data: alignmentFieldsRequest,
    });

    expect(createAlignmentFieldsRes.ok()).toBeTruthy();
    expect(createAlignmentFieldsRes.status()).toBe(200);

    // Step 7: Create fields for second PDF (field-meta fields)
    const fieldMetaFieldsRequest = {
      envelopeId: createdEnvelope.id,
      data: FIELD_META_TEST_FIELDS.map((field) => ({
        recipientId,
        envelopeItemId: fieldMetaItem.id,
        type: field.type,
        page: field.page,
        positionX: field.positionX,
        positionY: field.positionY,
        width: field.width,
        height: field.height,
        fieldMeta: field.fieldMeta,
      })),
    };

    const createFieldMetaFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
      headers: { Authorization: `Bearer ${tokenA}` },
      data: fieldMetaFieldsRequest,
    });

    expect(createFieldMetaFieldsRes.ok()).toBeTruthy();
    expect(createFieldMetaFieldsRes.status()).toBe(200);

    // Step 8: Verify final envelope structure
    const finalGetRes = await request.get(`${baseUrl}/envelope/${createdEnvelope.id}`, {
      headers: { Authorization: `Bearer ${tokenA}` },
    });

    expect(finalGetRes.ok()).toBeTruthy();
    const finalEnvelope = (await finalGetRes.json()) as TGetEnvelopeResponse;

    // Verify structure
    expect(finalEnvelope.envelopeItems.length).toBe(2);
    expect(finalEnvelope.recipients.length).toBe(1);
    expect(finalEnvelope.fields.length).toBe(ALIGNMENT_TEST_FIELDS.length + FIELD_META_TEST_FIELDS.length);
    expect(finalEnvelope.title).toBe('Envelope Full Field Test');
    expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);

    console.log({
      createdEnvelopeId: finalEnvelope.id,
      userEmail: userA.email,
    });
  });

  test.describe('Envelope find endpoint', () => {
    const createEnvelope = async (request: APIRequestContext, token: string, payload: TCreateEnvelopePayload) => {
      const formData = new FormData();
      formData.append('payload', JSON.stringify(payload));

      const pdfData = fs.readFileSync(path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'));
      formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' }));

      const res = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${token}` },
        multipart: formData,
      });

      expect(res.ok()).toBeTruthy();
      return (await res.json()) as TCreateEnvelopeResponse;
    };

    test('should find envelopes with pagination', async ({ request }) => {
      // Create 3 envelopes
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.DOCUMENT,
        title: 'Document 1',
      });
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.DOCUMENT,
        title: 'Document 2',
      });
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.TEMPLATE,
        title: 'Template 1',
      });

      // Find all envelopes
      const res = await request.get(`${baseUrl}/envelope`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      expect(res.ok()).toBeTruthy();
      expect(res.status()).toBe(200);

      const response = (await res.json()) as TFindEnvelopesResponse;

      expect(response.data.length).toBe(3);
      expect(response.count).toBe(3);
      expect(response.currentPage).toBe(1);
      expect(response.totalPages).toBe(1);

      // Test pagination
      const paginatedRes = await request.get(`${baseUrl}/envelope?perPage=2&page=1`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      expect(paginatedRes.ok()).toBeTruthy();
      const paginatedResponse = (await paginatedRes.json()) as TFindEnvelopesResponse;

      expect(paginatedResponse.data.length).toBe(2);
      expect(paginatedResponse.count).toBe(3);
      expect(paginatedResponse.totalPages).toBe(2);
    });

    test('should filter envelopes by type', async ({ request }) => {
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.DOCUMENT,
        title: 'Document Only',
      });
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.TEMPLATE,
        title: 'Template Only',
      });

      // Filter by DOCUMENT type
      const documentRes = await request.get(`${baseUrl}/envelope?type=DOCUMENT`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      expect(documentRes.ok()).toBeTruthy();
      const documentResponse = (await documentRes.json()) as TFindEnvelopesResponse;

      expect(documentResponse.data.every((e) => e.type === EnvelopeType.DOCUMENT)).toBe(true);

      // Filter by TEMPLATE type
      const templateRes = await request.get(`${baseUrl}/envelope?type=TEMPLATE`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      expect(templateRes.ok()).toBeTruthy();
      const templateResponse = (await templateRes.json()) as TFindEnvelopesResponse;

      expect(templateResponse.data.every((e) => e.type === EnvelopeType.TEMPLATE)).toBe(true);
    });

    test('should filter envelopes by status', async ({ request }) => {
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.DOCUMENT,
        title: 'Draft Document',
      });

      // Filter by DRAFT status (default for new envelopes)
      const res = await request.get(`${baseUrl}/envelope?status=DRAFT`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      expect(res.ok()).toBeTruthy();
      const response = (await res.json()) as TFindEnvelopesResponse;

      expect(response.data.every((e) => e.status === DocumentStatus.DRAFT)).toBe(true);
    });

    test('should search envelopes by query', async ({ request }) => {
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.DOCUMENT,
        title: 'Unique Searchable Title',
      });
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.DOCUMENT,
        title: 'Another Document',
      });

      const res = await request.get(`${baseUrl}/envelope?query=Unique%20Searchable`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      expect(res.ok()).toBeTruthy();
      const response = (await res.json()) as TFindEnvelopesResponse;

      expect(response.data.length).toBe(1);
      expect(response.data[0].title).toBe('Unique Searchable Title');
    });

    test('should not return envelopes from other users', async ({ request }) => {
      // Create envelope for userA
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.DOCUMENT,
        title: 'UserA Document',
      });

      // Create envelope for userB
      await createEnvelope(request, tokenB, {
        type: EnvelopeType.DOCUMENT,
        title: 'UserB Document',
      });

      // userA should only see their own envelopes
      const resA = await request.get(`${baseUrl}/envelope`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      expect(resA.ok()).toBeTruthy();
      const responseA = (await resA.json()) as TFindEnvelopesResponse;

      expect(responseA.data.every((e) => e.title !== 'UserB Document')).toBe(true);

      // userB should only see their own envelopes
      const resB = await request.get(`${baseUrl}/envelope`, {
        headers: { Authorization: `Bearer ${tokenB}` },
      });

      expect(resB.ok()).toBeTruthy();
      const responseB = (await resB.json()) as TFindEnvelopesResponse;

      expect(responseB.data.every((e) => e.title !== 'UserA Document')).toBe(true);
    });

    test('should return envelope with expected schema fields', async ({ request }) => {
      await createEnvelope(request, tokenA, {
        type: EnvelopeType.DOCUMENT,
        title: 'Schema Test Document',
      });

      const res = await request.get(`${baseUrl}/envelope`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      expect(res.ok()).toBeTruthy();
      const response = (await res.json()) as TFindEnvelopesResponse;

      const envelope = response.data.find((e) => e.title === 'Schema Test Document');

      expect(envelope).toBeDefined();
      expect(envelope?.id).toBeDefined();
      expect(envelope?.type).toBe(EnvelopeType.DOCUMENT);
      expect(envelope?.status).toBe(DocumentStatus.DRAFT);
      expect(envelope?.recipients).toBeDefined();
      expect(envelope?.user).toBeDefined();
      expect(envelope?.team).toBeDefined();
    });
  });

  test.describe('Empty recipient tests', () => {
    test('Create template envelope with empty email recipient', async ({ request }) => {
      const payload = {
        type: EnvelopeType.TEMPLATE,
        title: 'Template with Empty Email Recipient',
      } satisfies TCreateEnvelopePayload;

      const formData = new FormData();
      formData.append('payload', JSON.stringify(payload));

      const files = [
        {
          name: 'example.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
        },
      ];

      for (const file of files) {
        formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
      }

      const res = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        multipart: formData,
      });

      expect(res.ok()).toBeTruthy();
      expect(res.status()).toBe(200);

      const response = (await res.json()) as TCreateEnvelopeResponse;

      // Create recipient with empty email
      const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
        envelopeId: response.id,
        data: [
          {
            email: '',
            name: 'Test Recipient',
            role: RecipientRole.SIGNER,
            accessAuth: [],
            actionAuth: [],
          },
        ],
      };

      const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createRecipientsRequest,
      });

      expect(createRecipientsRes.ok()).toBeTruthy();
      expect(createRecipientsRes.status()).toBe(200);

      const recipientsResponse = await createRecipientsRes.json();
      const recipient = recipientsResponse.data[0];

      expect(recipient.email).toBe('');
      expect(recipient.name).toBe('Test Recipient');

      // Get envelope items to assign fields
      const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${response.id}`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
      const envelopeItem = envelope.envelopeItems[0];

      // Create field for the recipient with empty email
      const createFieldsRequest = {
        envelopeId: response.id,
        data: [
          {
            recipientId: recipient.id,
            envelopeItemId: envelopeItem.id,
            type: FieldType.SIGNATURE,
            page: 1,
            positionX: 100,
            positionY: 100,
            width: 50,
            height: 50,
          },
        ],
      };

      const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createFieldsRequest,
      });

      expect(createFieldsRes.ok()).toBeTruthy();
      expect(createFieldsRes.status()).toBe(200);
    });

    test('Create document envelope with empty email recipient', async ({ request }) => {
      const payload = {
        type: EnvelopeType.DOCUMENT,
        title: 'Document with Empty Email Recipient',
      } satisfies TCreateEnvelopePayload;

      const formData = new FormData();
      formData.append('payload', JSON.stringify(payload));

      const files = [
        {
          name: 'example.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
        },
      ];

      for (const file of files) {
        formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
      }

      const res = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        multipart: formData,
      });

      expect(res.ok()).toBeTruthy();
      expect(res.status()).toBe(200);

      const response = (await res.json()) as TCreateEnvelopeResponse;

      // Create recipient with empty email
      const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
        envelopeId: response.id,
        data: [
          {
            email: '',
            name: 'Document Recipient No Email',
            role: RecipientRole.SIGNER,
            accessAuth: [],
            actionAuth: [],
          },
        ],
      };

      const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createRecipientsRequest,
      });

      expect(createRecipientsRes.ok()).toBeTruthy();
      const recipientsResponse = await createRecipientsRes.json();
      const recipient = recipientsResponse.data[0];

      expect(recipient.email).toBe('');
      expect(recipient.name).toBe('Document Recipient No Email');
    });

    test('Update recipient to have empty email', async ({ request }) => {
      const payload = {
        type: EnvelopeType.TEMPLATE,
        title: 'Update Recipient Email Test',
        recipients: [
          {
            email: userA.email,
            name: 'Test User',
            role: RecipientRole.SIGNER,
          },
        ],
      } satisfies TCreateEnvelopePayload;

      const formData = new FormData();
      formData.append('payload', JSON.stringify(payload));

      const files = [
        {
          name: 'example.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
        },
      ];

      for (const file of files) {
        formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
      }

      const createRes = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        multipart: formData,
      });

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

      // Get the envelope to get recipient ID
      const getRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      const envelope: TGetEnvelopeResponse = await getRes.json();
      const recipientId = envelope.recipients[0].id;

      // Update recipient to have empty email
      const updateRequest: TUpdateEnvelopeRecipientsRequest = {
        envelopeId: createResponse.id,
        data: [
          {
            id: recipientId,
            email: '',
            name: 'Updated Name No Email',
          },
        ],
      };

      const updateRes = await request.post(`${baseUrl}/envelope/recipient/update-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: updateRequest,
      });

      expect(updateRes.ok()).toBeTruthy();
      const updateResponse = await updateRes.json();
      const updatedRecipient = updateResponse.data[0];

      expect(updatedRecipient.email).toBe('');
      expect(updatedRecipient.name).toBe('Updated Name No Email');
    });

    test('Mixed recipients with and without emails', async ({ request }) => {
      const payload = {
        type: EnvelopeType.TEMPLATE,
        title: 'Mixed Recipients Test',
      } satisfies TCreateEnvelopePayload;

      const formData = new FormData();
      formData.append('payload', JSON.stringify(payload));

      const files = [
        {
          name: 'example.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
        },
      ];

      for (const file of files) {
        formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
      }

      const res = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        multipart: formData,
      });

      expect(res.ok()).toBeTruthy();
      const response = (await res.json()) as TCreateEnvelopeResponse;

      // Create multiple recipients, some with email, some without
      const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
        envelopeId: response.id,
        data: [
          {
            email: userA.email,
            name: 'Recipient With Email',
            role: RecipientRole.SIGNER,
            accessAuth: [],
            actionAuth: [],
          },
          {
            email: '',
            name: 'Recipient Without Email 1',
            role: RecipientRole.SIGNER,
            accessAuth: [],
            actionAuth: [],
          },
          {
            email: userB.email,
            name: 'Another With Email',
            role: RecipientRole.APPROVER,
            accessAuth: [],
            actionAuth: [],
          },
          {
            email: '',
            name: 'Recipient Without Email 2',
            role: RecipientRole.SIGNER,
            accessAuth: [],
            actionAuth: [],
          },
        ],
      };

      const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createRecipientsRequest,
      });

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

      expect(recipients.length).toBe(4);
      expect(recipients[0].email).toBe(userA.email.toLowerCase());
      expect(recipients[1].email).toBe('');
      expect(recipients[2].email).toBe(userB.email.toLowerCase());
      expect(recipients[3].email).toBe('');

      // Get envelope to assign fields
      const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${response.id}`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
      const envelopeItem = envelope.envelopeItems[0];

      // Create fields for all recipients including those without emails
      const createFieldsRequest = {
        envelopeId: response.id,
        data: recipients.map((recipient, index) => ({
          recipientId: recipient.id,
          envelopeItemId: envelopeItem.id,
          type: FieldType.SIGNATURE,
          page: 1,
          positionX: 100,
          positionY: 0 + index,
          width: 50,
          height: 50,
        })),
      };

      const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createFieldsRequest,
      });

      expect(createFieldsRes.ok()).toBeTruthy();
    });

    test('Distribute envelope with empty email recipients', async ({ request }) => {
      const payload = {
        type: EnvelopeType.DOCUMENT,
        title: 'Document for Distribution with Empty Email',
      } satisfies TCreateEnvelopePayload;

      const formData = new FormData();
      formData.append('payload', JSON.stringify(payload));

      const files = [
        {
          name: 'example.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
        },
      ];

      for (const file of files) {
        formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
      }

      const createRes = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        multipart: formData,
      });

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

      // Create recipients with empty emails
      const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
        envelopeId: createResponse.id,
        data: [
          {
            email: '',
            name: 'Recipient One',
            role: RecipientRole.SIGNER,
            accessAuth: [],
            actionAuth: [],
          },
          {
            email: '',
            name: 'Recipient Two',
            role: RecipientRole.APPROVER,
            accessAuth: [],
            actionAuth: [],
          },
        ],
      };

      const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createRecipientsRequest,
      });

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

      // Get envelope to assign fields
      const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
      const envelopeItem = envelope.envelopeItems[0];

      // Create fields for recipients
      const createFieldsRequest = {
        envelopeId: createResponse.id,
        data: recipients.map((recipient, index) => ({
          recipientId: recipient.id,
          envelopeItemId: envelopeItem.id,
          type: FieldType.SIGNATURE,
          page: 1,
          positionX: 100,
          positionY: 0 + index,
          width: 50,
          height: 50,
        })),
      };

      const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createFieldsRequest,
      });

      expect(createFieldsRes.ok()).toBeTruthy();

      // Distribute the envelope
      const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: {
          envelopeId: createResponse.id,
        } satisfies TDistributeEnvelopeRequest,
      });

      expect(distributeRes.ok()).toBeTruthy();
      expect(distributeRes.status()).toBe(200);

      const distributeResponse = await distributeRes.json();
      expect(distributeResponse.success).toBe(true);
      expect(distributeResponse.id).toBe(createResponse.id);
      expect(distributeResponse.recipients).toHaveLength(2);

      // Verify recipients have empty emails and signing URLs
      expect(distributeResponse.recipients[0].email).toBe('');
      expect(distributeResponse.recipients[0].signingUrl).toBeTruthy();
      expect(distributeResponse.recipients[1].email).toBe('');
      expect(distributeResponse.recipients[1].signingUrl).toBeTruthy();
    });

    test('Distribute envelope with empty email recipient and auth requirements fails', async ({ request }) => {
      const payload = {
        type: EnvelopeType.DOCUMENT,
        title: 'Document with Auth Requirements',
      } satisfies TCreateEnvelopePayload;

      const formData = new FormData();
      formData.append('payload', JSON.stringify(payload));

      const files = [
        {
          name: 'example.pdf',
          data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
        },
      ];

      for (const file of files) {
        formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
      }

      const createRes = await request.post(`${baseUrl}/envelope/create`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        multipart: formData,
      });

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

      // Create recipient with empty email and TWO_FACTOR_AUTH action auth
      const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
        envelopeId: createResponse.id,
        data: [
          {
            email: '',
            name: 'Recipient With Auth',
            role: RecipientRole.SIGNER,
            accessAuth: [DocumentAccessAuth.TWO_FACTOR_AUTH],
          },
        ],
      };

      const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createRecipientsRequest,
      });

      expect(createRecipientsRes.ok()).toBeTruthy();
      const recipientsResponse = await createRecipientsRes.json();
      const recipient = recipientsResponse.data[0];

      // Get envelope to assign fields
      const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
        headers: { Authorization: `Bearer ${tokenA}` },
      });

      const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
      const envelopeItem = envelope.envelopeItems[0];

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

      const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: createFieldsRequest,
      });

      expect(createFieldsRes.ok()).toBeTruthy();

      // Try to distribute the envelope - should fail
      const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
        headers: { Authorization: `Bearer ${tokenA}` },
        data: {
          envelopeId: createResponse.id,
        },
      });

      // Expect distribution to fail
      expect(distributeRes.ok()).toBeFalsy();
      expect(distributeRes.status()).toBe(400);

      const errorResponse = await distributeRes.json();
      expect(errorResponse.message).toContain('requires an email');
    });
  });
});
