/**
 * Unit tests for the version-aware Documenso placement abstraction.
 * Covers v1/v2 dispatch, percent→pixel coord conversion for v1, and the pure
 * default-signature layout math for 1/2/3/5 recipients.
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('@/lib/services/port-config', () => ({
  getPortDocumensoConfig: vi.fn(),
}));

import * as portConfig from '@/lib/services/port-config';
import {
  __resetDocumensoCachesForTests,
  computeDefaultSignatureLayout,
  placeFields,
  placeDefaultSignatureFields,
  voidDocument,
} from '@/lib/services/documenso-client';

const fetchMock = vi.fn();

beforeEach(() => {
  vi.stubGlobal('fetch', fetchMock);
  __resetDocumensoCachesForTests();
});

afterEach(() => {
  fetchMock.mockReset();
  vi.unstubAllGlobals();
  vi.mocked(portConfig.getPortDocumensoConfig).mockReset();
});

function configurePort(version: 'v1' | 'v2'): void {
  vi.mocked(portConfig.getPortDocumensoConfig).mockResolvedValue({
    apiUrl: 'https://documenso.test',
    apiKey: 'sk_test',
    apiKeySource: 'port',
    apiUrlSource: 'port',
    apiVersion: version,
    eoiTemplateId: 8,
    defaultPathway: 'documenso-template',
    eoiFillMethod: 'local',
    clientRecipientId: 192,
    developerRecipientId: 193,
    approvalRecipientId: 194,
    developerName: 'Test Developer',
    developerEmail: 'dev@test.invalid',
    approverName: 'Test Approver',
    approverEmail: 'approver@test.invalid',
    sendMode: 'manual',
    embeddedSigningHost: null,
    contractTemplateId: null,
    reservationTemplateId: null,
    developerLabel: 'Developer',
    approverLabel: 'Approver',
    developerUserId: null,
    approverUserId: null,
    signingOrder: null,
    redirectUrl: null,
  });
}

function okResponse(body: unknown = {}): Response {
  return new Response(JSON.stringify(body), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  });
}

describe('computeDefaultSignatureLayout', () => {
  it('returns one centered field for a single recipient', () => {
    const fields = computeDefaultSignatureLayout([{ id: 1, pageNumber: 3 }]);
    expect(fields).toHaveLength(1);
    expect(fields[0]).toMatchObject({
      recipientId: 1,
      type: 'SIGNATURE',
      pageNumber: 3,
      pageWidth: 20, // 80/1 capped at 20
      pageHeight: 6,
      pageY: 88,
    });
    expect(fields[0]!.pageX).toBeCloseTo(40, 5); // 50 - 20/2
  });

  it('staggers two recipients without overlap', () => {
    const fields = computeDefaultSignatureLayout([
      { id: 1, pageNumber: 1 },
      { id: 2, pageNumber: 1 },
    ]);
    expect(fields).toHaveLength(2);
    expect(fields[1]!.pageX).toBeGreaterThan(fields[0]!.pageX + fields[0]!.pageWidth - 0.001);
  });

  it('keeps total row width <= 80% for 5 recipients', () => {
    const fields = computeDefaultSignatureLayout(
      [1, 2, 3, 4, 5].map((id) => ({ id, pageNumber: 1 })),
    );
    const totalWidth = fields[fields.length - 1]!.pageX + fields[0]!.pageWidth - fields[0]!.pageX;
    expect(totalWidth).toBeLessThanOrEqual(80 + 0.001);
    expect(fields.every((f) => f.pageX >= 0)).toBe(true);
    expect(fields.every((f) => f.pageX + f.pageWidth <= 100)).toBe(true);
  });

  it('returns empty array for zero recipients', () => {
    expect(computeDefaultSignatureLayout([])).toEqual([]);
  });
});

describe('placeFields v2 dispatch', () => {
  beforeEach(() => configurePort('v2'));

  it('makes a single bulk POST to envelope/field/create-many', async () => {
    fetchMock.mockResolvedValueOnce(okResponse());
    await placeFields(
      'env-123',
      [
        {
          // v2 recipient ids are numeric - Documenso's distribute response
          // returns them as numbers. The CRM custom-document-upload
          // service preserves them as strings or numbers; the v2 placeFields
          // coercion normalises to number for the upstream payload.
          recipientId: '42',
          type: 'SIGNATURE',
          pageNumber: 1,
          pageX: 25,
          pageY: 88,
          pageWidth: 20,
          pageHeight: 6,
          fieldMeta: { label: 'Sign here' },
        },
      ],
      'port-1',
    );

    expect(fetchMock).toHaveBeenCalledTimes(1);
    const [url, init] = fetchMock.mock.calls[0]!;
    expect(url).toBe('https://documenso.test/api/v2/envelope/field/create-many');
    expect((init as RequestInit).method).toBe('POST');
    const body = JSON.parse(String((init as RequestInit).body)) as any;
    expect(body.envelopeId).toBe('env-123');
    // 2026-05-22: Documenso v2 expects the array under `data` (trpc-style
    // createMany input), not `fields`. recipientId is a number, and the
    // page-index key is `page` (not `pageNumber`).
    expect(body.data[0]).toMatchObject({
      recipientId: 42,
      type: 'SIGNATURE',
      page: 1,
      positionX: 25,
      positionY: 88,
      width: 20,
      height: 6,
      fieldMeta: { label: 'Sign here' },
    });
  });

  it('throws on non-2xx response', async () => {
    fetchMock.mockResolvedValueOnce(new Response('boom', { status: 500 }));
    await expect(
      placeFields(
        'env-123',
        [
          {
            recipientId: 'rec-a',
            type: 'SIGNATURE',
            pageNumber: 1,
            pageX: 0,
            pageY: 0,
            pageWidth: 10,
            pageHeight: 10,
          },
        ],
        'port-1',
      ),
    ).rejects.toThrow(/signing service didn't respond/);
  });
});

describe('placeFields v1 dispatch', () => {
  beforeEach(() => configurePort('v1'));

  it('issues one POST per field with pixel coords on a default A4 page', async () => {
    fetchMock.mockResolvedValue(okResponse());
    await placeFields(
      'doc-123',
      [
        {
          recipientId: 42,
          type: 'SIGNATURE',
          pageNumber: 1,
          pageX: 50, // 50% of 595 = 298 (rounded)
          pageY: 88, // 88% of 842 = 741
          pageWidth: 20, // 20% of 595 = 119
          pageHeight: 6, // 6% of 842 = 51
        },
        {
          recipientId: 43,
          type: 'TEXT',
          pageNumber: 2,
          pageX: 10,
          pageY: 10,
          pageWidth: 30,
          pageHeight: 5,
        },
      ],
      'port-1',
    );

    expect(fetchMock).toHaveBeenCalledTimes(2);
    const firstCall = fetchMock.mock.calls[0]!;
    expect(firstCall[0]).toBe('https://documenso.test/api/v1/documents/doc-123/fields');
    const firstBody = JSON.parse(String((firstCall[1] as RequestInit).body)) as any;
    expect(firstBody).toMatchObject({
      recipientId: 42,
      type: 'SIGNATURE',
      pageNumber: 1,
    });
    expect(firstBody.pageX).toBe(298);
    expect(firstBody.pageY).toBe(741);
    expect(firstBody.pageWidth).toBe(119);
    expect(firstBody.pageHeight).toBe(51);
  });

  it('coerces string recipientId to number on v1', async () => {
    fetchMock.mockResolvedValue(okResponse());
    await placeFields(
      'doc-1',
      [
        {
          recipientId: '99',
          type: 'SIGNATURE',
          pageNumber: 1,
          pageX: 0,
          pageY: 0,
          pageWidth: 1,
          pageHeight: 1,
        },
      ],
      'port-1',
    );
    const body = JSON.parse(String((fetchMock.mock.calls[0]![1] as RequestInit).body)) as any;
    expect(body.recipientId).toBe(99);
  });

  it('throws on non-2xx response', async () => {
    fetchMock.mockResolvedValueOnce(new Response('nope', { status: 422 }));
    await expect(
      placeFields(
        'doc-1',
        [
          {
            recipientId: 1,
            type: 'SIGNATURE',
            pageNumber: 1,
            pageX: 0,
            pageY: 0,
            pageWidth: 1,
            pageHeight: 1,
          },
        ],
        'port-1',
      ),
    ).rejects.toThrow(/signing service didn't respond/);
  });
});

describe('placeDefaultSignatureFields integration', () => {
  it('places staggered defaults on v2 envelope', async () => {
    configurePort('v2');
    fetchMock.mockResolvedValueOnce(okResponse());
    await placeDefaultSignatureFields(
      'env-x',
      [
        { id: '101', pageNumber: 4 },
        { id: '102', pageNumber: 4 },
        { id: '103', pageNumber: 4 },
      ],
      'port-1',
    );
    expect(fetchMock).toHaveBeenCalledTimes(1);
    const body = JSON.parse(String((fetchMock.mock.calls[0]![1] as RequestInit).body)) as any;
    // 2026-05-22: Documenso v2 expects `data` (not `fields`), `page`
    // (not `pageNumber`), and numeric recipientIds.
    expect(body.data).toHaveLength(3);
    expect(body.data.every((f: { type: string }) => f.type === 'SIGNATURE')).toBe(true);
    expect(body.data.every((f: { page: number }) => f.page === 4)).toBe(true);
    expect(
      body.data.every((f: { recipientId: unknown }) => typeof f.recipientId === 'number'),
    ).toBe(true);
  });

  it('skips the API call entirely with zero recipients', async () => {
    configurePort('v1');
    await placeDefaultSignatureFields('doc-y', [], 'port-1');
    expect(fetchMock).not.toHaveBeenCalled();
  });

  it('issues N per-field POSTs with pixel-converted coords on v1', async () => {
    configurePort('v1');
    fetchMock.mockResolvedValue(okResponse());
    await placeDefaultSignatureFields(
      'doc-z',
      [
        { id: 7, pageNumber: 1 },
        { id: 8, pageNumber: 1 },
      ],
      'port-1',
    );
    expect(fetchMock).toHaveBeenCalledTimes(2);
    for (const call of fetchMock.mock.calls) {
      expect(call[0]).toBe('https://documenso.test/api/v1/documents/doc-z/fields');
      const body = JSON.parse(String((call[1] as RequestInit).body)) as any;
      expect(body.type).toBe('SIGNATURE');
      expect(body.pageNumber).toBe(1);
      // 88% of 842 = 741 (footer band)
      expect(body.pageY).toBe(741);
      // height = 6% of 842 = 51
      expect(body.pageHeight).toBe(51);
      // width = 20% of 595 = 119
      expect(body.pageWidth).toBe(119);
    }
  });
});

describe('voidDocument', () => {
  it('issues DELETE to /api/v1/documents/{id} on v1', async () => {
    configurePort('v1');
    fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }));
    await voidDocument('doc-1', 'port-1');
    expect(fetchMock).toHaveBeenCalledWith(
      'https://documenso.test/api/v1/documents/doc-1',
      expect.objectContaining({ method: 'DELETE' }),
    );
  });

  it('issues DELETE to /api/v2/envelope/{id} on v2', async () => {
    configurePort('v2');
    fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }));
    await voidDocument('env-1', 'port-1');
    expect(fetchMock).toHaveBeenCalledWith(
      'https://documenso.test/api/v2/envelope/env-1',
      expect.objectContaining({ method: 'DELETE' }),
    );
  });

  it('treats 404 as idempotent success', async () => {
    configurePort('v1');
    fetchMock.mockResolvedValueOnce(new Response('not found', { status: 404 }));
    await expect(voidDocument('doc-1', 'port-1')).resolves.toBeUndefined();
  });

  it('throws on other non-2xx responses', async () => {
    configurePort('v2');
    fetchMock.mockResolvedValueOnce(new Response('boom', { status: 500 }));
    await expect(voidDocument('env-1', 'port-1')).rejects.toThrow(/signing service didn't respond/);
  });
});
