/**
 * Integration test: external-EOI upload stage-advance gate.
 *
 * Regression coverage for the 2026-05-24 critical bug — the previous code
 * gated on the legacy 9-stage vocabulary (`open` / `details_sent` /
 * `in_communication` / `eoi_sent`), so uploads against canonical pre-EOI
 * stages (`enquiry`, `qualified`, `nurturing`) left `pipeline_stage` stuck
 * while `eoi_status` flipped to `signed`.
 *
 * Asserts:
 *  - canonical pre-EOI stages (enquiry / qualified / nurturing) advance to `eoi`
 *  - at-or-past-EOI stages (eoi / reservation / deposit_paid / contract) stay put
 *  - document-metadata writes (dateEoiSigned, eoiStatus, eoiDocStatus) fire on every path
 *  - the service return reports stageChanged + newStage accurately
 *
 * Storage is mocked to a no-op MinIO put so the service can run without S3.
 */

import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { db } from '@/lib/db';
import { interests } from '@/lib/db/schema/interests';
import { documents } from '@/lib/db/schema/documents';
import { eq } from 'drizzle-orm';

let uploadExternallySignedEoi: typeof import('@/lib/services/external-eoi.service').uploadExternallySignedEoi;
let createInterest: typeof import('@/lib/services/interests.service').createInterest;
let makePort: typeof import('../helpers/factories').makePort;
let makeClient: typeof import('../helpers/factories').makeClient;
let makeAuditMeta: typeof import('../helpers/factories').makeAuditMeta;

beforeAll(async () => {
  const ext = await import('@/lib/services/external-eoi.service');
  uploadExternallySignedEoi = ext.uploadExternallySignedEoi;
  const svc = await import('@/lib/services/interests.service');
  createInterest = svc.createInterest;
  const factories = await import('../helpers/factories');
  makePort = factories.makePort;
  makeClient = factories.makeClient;
  makeAuditMeta = factories.makeAuditMeta;
});

describe('uploadExternallySignedEoi — stage-advance gate', () => {
  beforeEach(() => {
    vi.doMock('@/lib/storage', async () => {
      const real = await vi.importActual<typeof import('@/lib/storage')>('@/lib/storage');
      return {
        ...real,
        getStorageBackend: vi.fn(async () => ({
          put: vi.fn(async () => undefined),
          get: vi.fn(),
          head: vi.fn(),
          delete: vi.fn(async () => undefined),
          listByPrefix: vi.fn(async () => []),
          presignUpload: vi.fn(async () => ''),
          presignDownload: vi.fn(async () => ''),
        })),
      };
    });
  });

  afterEach(() => {
    vi.doUnmock('@/lib/storage');
  });

  async function makeInterest(
    stage:
      | 'enquiry'
      | 'qualified'
      | 'nurturing'
      | 'eoi'
      | 'reservation'
      | 'deposit_paid'
      | 'contract',
  ) {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });
    const meta = makeAuditMeta({ portId: port.id });

    const created = await createInterest(
      port.id,
      { clientId: client.id, pipelineStage: 'enquiry', reminderEnabled: false, tagIds: [] },
      meta,
    );

    // Set stage directly via DB to bypass the yacht-required gate on
    // changeInterestStage — this test focuses solely on the external-EOI
    // advance behaviour given an arbitrary starting stage, not on the
    // separate yacht-link gate that fronts manual stage transitions.
    if (stage !== 'enquiry') {
      await db.update(interests).set({ pipelineStage: stage }).where(eq(interests.id, created.id));
    }

    return { port, client, meta, interest: created };
  }

  async function uploadFakeEoi(
    interestId: string,
    portId: string,
    meta: ReturnType<typeof makeAuditMeta>,
    docType?: 'eoi' | 'reservation' | 'contract',
  ) {
    return uploadExternallySignedEoi({
      interestId,
      portId,
      fileData: {
        buffer: Buffer.from('%PDF-1.4\n%fake\n'),
        originalName: 'signed.pdf',
        mimeType: 'application/pdf',
        size: 16,
      },
      signedAt: new Date('2026-05-24T10:00:00Z'),
      signatories: [{ name: 'Client', email: 'client@example.com', role: 'client' }],
      ...(docType ? { docType } : {}),
      meta,
    });
  }

  it.each(['enquiry', 'qualified', 'nurturing'] as const)(
    'advances %s → eoi',
    async (startStage) => {
      const { port, meta, interest } = await makeInterest(startStage);

      const result = await uploadFakeEoi(interest.id, port.id, meta);

      expect(result.stageChanged).toBe(true);
      expect(result.newStage).toBe('eoi');

      const row = await db.query.interests.findFirst({ where: eq(interests.id, interest.id) });
      expect(row?.pipelineStage).toBe('eoi');
      expect(row?.eoiStatus).toBe('signed');
      expect(row?.eoiDocStatus).toBe('signed');
      expect(row?.dateEoiSigned).toBeTruthy();
    },
  );

  it.each(['eoi', 'reservation', 'deposit_paid', 'contract'] as const)(
    'leaves %s untouched (metadata still writes)',
    async (startStage) => {
      const { port, meta, interest } = await makeInterest(startStage);

      const result = await uploadFakeEoi(interest.id, port.id, meta);

      expect(result.stageChanged).toBe(false);
      expect(result.newStage).toBe(startStage);

      const row = await db.query.interests.findFirst({ where: eq(interests.id, interest.id) });
      expect(row?.pipelineStage).toBe(startStage);
      expect(row?.eoiStatus).toBe('signed');
      expect(row?.eoiDocStatus).toBe('signed');
      expect(row?.dateEoiSigned).toBeTruthy();
    },
  );

  // ── docType routing ────────────────────────────────────────────────────────
  // Regression coverage for the paper-upload misroute: the reservation and
  // contract tabs reused this dialog, but the service hard-coded the EOI
  // documentType / status columns / stage target. A signed contract uploaded
  // from the Contract tab filed as an `eoi` and flipped `eoi_status` — wrong
  // doc kind, wrong sub-state, wrong stage. The service now keys all of that
  // off `docType`.
  describe('docType routing (reservation / contract)', () => {
    it('reservation: files as reservation_agreement, advances eoi → reservation, leaves eoi_status null', async () => {
      const { port, meta, interest } = await makeInterest('eoi');

      const result = await uploadFakeEoi(interest.id, port.id, meta, 'reservation');

      expect(result.stageChanged).toBe(true);
      expect(result.newStage).toBe('reservation');

      const row = await db.query.interests.findFirst({ where: eq(interests.id, interest.id) });
      expect(row?.pipelineStage).toBe('reservation');
      expect(row?.reservationDocStatus).toBe('signed');
      expect(row?.dateReservationSigned).toBeTruthy();
      // The EOI lifecycle column must NOT be touched by a reservation upload.
      expect(row?.eoiStatus).toBeNull();
      expect(row?.eoiDocStatus).toBeNull();

      const doc = await db.query.documents.findFirst({
        where: eq(documents.id, result.documentId),
      });
      expect(doc?.documentType).toBe('reservation_agreement');
    });

    it('contract: files as contract, advances reservation → contract, sets contract columns only', async () => {
      const { port, meta, interest } = await makeInterest('reservation');

      const result = await uploadFakeEoi(interest.id, port.id, meta, 'contract');

      expect(result.stageChanged).toBe(true);
      expect(result.newStage).toBe('contract');

      const row = await db.query.interests.findFirst({ where: eq(interests.id, interest.id) });
      expect(row?.pipelineStage).toBe('contract');
      expect(row?.contractDocStatus).toBe('signed');
      expect(row?.dateContractSigned).toBeTruthy();
      expect(row?.eoiStatus).toBeNull();

      const doc = await db.query.documents.findFirst({
        where: eq(documents.id, result.documentId),
      });
      expect(doc?.documentType).toBe('contract');
    });

    it('contract: at-or-past contract stage stays put (idempotent), still files the doc', async () => {
      const { port, meta, interest } = await makeInterest('contract');

      const result = await uploadFakeEoi(interest.id, port.id, meta, 'contract');

      expect(result.stageChanged).toBe(false);
      expect(result.newStage).toBe('contract');

      const row = await db.query.interests.findFirst({ where: eq(interests.id, interest.id) });
      expect(row?.pipelineStage).toBe('contract');
      expect(row?.contractDocStatus).toBe('signed');
    });
  });
});
