/**
 * Tests for the Documenso webhook receiver route at
 * `src/app/api/webhooks/documenso/route.ts`.
 *
 * The receiver was previously only covered indirectly: the unit test
 * `webhook-event-map.test.ts` validates the static event-name map, and
 * `documents-expired-webhook.test.ts` calls handlers directly. Neither
 * exercised the route's auth, dedup, dispatch loop, or 200-on-failure
 * envelope. This file fills that gap.
 *
 * Refs: docs/audit-comprehensive-2026-05-05.md HIGH §19 (auditor-J Issue 2).
 */
import { describe, it, expect } from 'vitest';
import { eq } from 'drizzle-orm';
import { NextRequest } from 'next/server';

import { POST as documensoWebhook } from '@/app/api/webhooks/documenso/route';
import { db } from '@/lib/db';
import { documents, documentEvents } from '@/lib/db/schema/documents';
import { interests } from '@/lib/db/schema/interests';
import { systemSettings } from '@/lib/db/schema/system';
import { encrypt } from '@/lib/utils/encryption';
import { makeClient, makePort } from '../helpers/factories';

/**
 * Seed a per-port webhook secret on the freshly-created test port and
 * return both the port + the plaintext secret. The receiver iterates
 * per-port secrets BEFORE the env fallback, so without this seed
 * `listDocumensoWebhookSecrets()` resolves the matched port to whichever
 * other port happens to share the env value (e.g. the dev `port-amador`
 * row that ships with the .env example). Each test gets a unique
 * secret so cross-test pollution can't happen either.
 */
async function seedWebhookSecret(portId: string): Promise<string> {
  const secret = `test-webhook-${portId.slice(0, 8)}-${Date.now()}`.padEnd(48, 'x').slice(0, 48);
  const envelope = JSON.parse(encrypt(secret));
  await db.insert(systemSettings).values({
    portId,
    key: 'documenso_webhook_secret',
    value: envelope,
  });
  return secret;
}

function buildRequest(body: Record<string, unknown>, secret: string): NextRequest {
  return new NextRequest('http://localhost:3000/api/webhooks/documenso', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-documenso-secret': secret,
    },
    body: JSON.stringify(body),
  });
}

describe('Documenso webhook route', () => {
  it('returns 200 with ok:false when the secret header is missing or wrong', async () => {
    const req = buildRequest(
      { event: 'DOCUMENT_SIGNED', payload: { id: 'abc', recipients: [] } },
      'wrong-secret',
    );
    const res = await documensoWebhook(req);
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toMatchObject({ ok: false });
  });

  it('with a valid secret + DOCUMENT_SIGNED writes a documentEvents row', async () => {
    const port = await makePort();
    const secret = await seedWebhookSecret(port.id);
    const client = await makeClient({ portId: port.id });

    const documensoId = `docu-test-${Date.now()}`;
    const [doc] = await db
      .insert(documents)
      .values({
        portId: port.id,
        clientId: client.id,
        documentType: 'eoi',
        title: 'Webhook test EOI',
        status: 'sent',
        documensoId,
        createdBy: 'seed',
      })
      .returning();

    const req = buildRequest(
      {
        event: 'DOCUMENT_SIGNED',
        payload: {
          id: documensoId,
          recipients: [{ email: 'signer@test.invalid', signingStatus: 'SIGNED' }],
        },
      },
      secret,
    );
    const res = await documensoWebhook(req);
    expect(res.status).toBe(200);

    const events = await db
      .select()
      .from(documentEvents)
      .where(eq(documentEvents.documentId, doc!.id));
    expect(events.length).toBeGreaterThanOrEqual(1);
  });

  it('DOCUMENT_COMPLETED for a reservation_agreement stamps reservationDocStatus on the linked interest', async () => {
    const port = await makePort();
    const secret = await seedWebhookSecret(port.id);
    const client = await makeClient({ portId: port.id });

    const [interest] = await db
      .insert(interests)
      .values({
        portId: port.id,
        clientId: client.id,
        pipelineStage: 'reservation',
      })
      .returning();

    const documensoId = `docu-resv-${Date.now()}`;
    await db.insert(documents).values({
      portId: port.id,
      clientId: client.id,
      interestId: interest!.id,
      documentType: 'reservation_agreement',
      title: 'Reservation webhook test',
      status: 'sent',
      documensoId,
      createdBy: 'seed',
    });

    const req = buildRequest(
      {
        event: 'DOCUMENT_COMPLETED',
        payload: { id: documensoId, recipients: [] },
      },
      secret,
    );
    const res = await documensoWebhook(req);
    expect(res.status).toBe(200);

    const [updated] = await db.select().from(interests).where(eq(interests.id, interest!.id));
    expect(updated?.reservationDocStatus).toBe('signed');
    expect(updated?.dateReservationSigned).not.toBeNull();
  });

  it('DOCUMENT_COMPLETED for a contract stamps contractDocStatus on the linked interest', async () => {
    const port = await makePort();
    const secret = await seedWebhookSecret(port.id);
    const client = await makeClient({ portId: port.id });

    const [interest] = await db
      .insert(interests)
      .values({
        portId: port.id,
        clientId: client.id,
        pipelineStage: 'contract',
      })
      .returning();

    const documensoId = `docu-contract-${Date.now()}`;
    await db.insert(documents).values({
      portId: port.id,
      clientId: client.id,
      interestId: interest!.id,
      documentType: 'contract',
      title: 'Contract webhook test',
      status: 'sent',
      documensoId,
      createdBy: 'seed',
    });

    const req = buildRequest(
      {
        event: 'DOCUMENT_COMPLETED',
        payload: { id: documensoId, recipients: [] },
      },
      secret,
    );
    const res = await documensoWebhook(req);
    expect(res.status).toBe(200);

    const [updated] = await db.select().from(interests).where(eq(interests.id, interest!.id));
    expect(updated?.contractDocStatus).toBe('signed');
    expect(updated?.dateContractSigned).not.toBeNull();
  });

  it('replays of the same body are no-ops (signatureHash dedup)', async () => {
    const port = await makePort();
    const secret = await seedWebhookSecret(port.id);
    const client = await makeClient({ portId: port.id });

    const documensoId = `docu-dedup-${Date.now()}`;
    const [doc] = await db
      .insert(documents)
      .values({
        portId: port.id,
        clientId: client.id,
        documentType: 'eoi',
        title: 'Dedup test EOI',
        status: 'sent',
        documensoId,
        createdBy: 'seed',
      })
      .returning();

    const body = {
      event: 'DOCUMENT_OPENED',
      payload: {
        id: documensoId,
        recipients: [{ email: 'opener@test.invalid', readStatus: 'OPENED' }],
      },
    };

    await documensoWebhook(buildRequest(body, secret));
    await documensoWebhook(buildRequest(body, secret));

    const events = await db
      .select()
      .from(documentEvents)
      .where(eq(documentEvents.documentId, doc!.id));
    // The route's `handleDocumentOpened` writes an event with type
    // `'viewed'`. One row from the first call; the second should have
    // been refused by the signatureHash dedup guard.
    const viewedEvents = events.filter((e) => e.eventType === 'viewed');
    expect(viewedEvents.length).toBe(1);
  });
});
