/**
 * Migration transform - fixture-based regression test.
 *
 * Feeds the transform a small frozen NocoDB snapshot containing one
 * representative row from each duplicate pattern documented in
 * docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §1.2,
 * and asserts the resulting plan matches the algorithm's expected
 * behavior. If any future change starts merging Pattern F (Etiennette
 * Clamouze) or stops merging Pattern A (Deepak Ramchandani), this
 * test fails immediately.
 */
import { describe, expect, it } from 'vitest';

import { transformSnapshot } from '@/lib/dedup/migration-transform';
import type { NocoDbRow, NocoDbSnapshot } from '@/lib/dedup/nocodb-source';

function row(fields: Partial<NocoDbRow> & { Id: number }): NocoDbRow {
  return fields as NocoDbRow;
}

const FIXTURE: NocoDbSnapshot = {
  fetchedAt: '2026-05-03T12:00:00.000Z',
  berths: [],
  residentialInterests: [],
  websiteInterestSubmissions: [],
  websiteContactFormSubmissions: [],
  websiteBerthEoiSupplements: [],
  interests: [
    // Pattern A: pure double-submit (Deepak Ramchandani #624/#625)
    row({
      Id: 624,
      'Full Name': 'Deepak Ramchandani',
      'Email Address': 'dannyrams8888@gmail.com',
      'Phone Number': '+17215868888',
      'Sales Process Level': 'General Qualified Interest',
    }),
    row({
      Id: 625,
      'Full Name': 'Deepak Ramchandani',
      'Email Address': 'dannyrams8888@gmail.com',
      'Phone Number': '+17215868888',
      'Sales Process Level': 'General Qualified Interest',
    }),

    // Pattern B: phone format variance (Howard Wiarda #236/#536)
    row({
      Id: 236,
      'Full Name': 'Howard Wiarda',
      'Email Address': 'hwiarda@hotmail.com',
      'Phone Number': '574-274-0548',
      'Place of Residence': 'USA',
      'Sales Process Level': 'General Qualified Interest',
    }),
    row({
      Id: 536,
      'Full Name': 'Howard Wiarda',
      'Email Address': 'hwiarda@hotmail.com',
      'Phone Number': '+15742740548',
      'Sales Process Level': 'General Qualified Interest',
    }),

    // Pattern C: name capitalization (Nicolas Ruiz #681/#682/#683 - three rows)
    row({
      Id: 681,
      'Full Name': 'Nicolas Ruiz',
      'Email Address': 'ruiz.nicolas@ufl.edu',
      'Phone Number': '+17862006617',
      'Sales Process Level': 'General Qualified Interest',
    }),
    row({
      Id: 682,
      'Full Name': 'Nicolas Ruiz',
      'Email Address': 'ruiz.nicolas@ufl.edu',
      'Phone Number': '+17862006617',
      'Sales Process Level': 'Specific Qualified Interest',
    }),
    row({
      Id: 683,
      'Full Name': 'Nicolas Ruiz',
      'Email Address': 'Ruiz.Nicolas@ufl.edu',
      'Phone Number': '+17862006617',
      'Sales Process Level': 'General Qualified Interest',
    }),

    // Pattern E: surname typo with same email + phone (Constanzo/Costanzo)
    row({
      Id: 336,
      'Full Name': 'Gianfranco Di Costanzo',
      'Email Address': 'gdc@nauticall.com',
      'Phone Number': '+17542628669',
      'Yacht Name': 'GEMINI',
      'Sales Process Level': 'Contract Signed',
    }),
    row({
      Id: 585,
      'Full Name': 'Gianfranco Di Constanzo',
      'Email Address': 'gdc@nauticall.com',
      'Phone Number': '+17542628669',
      'Yacht Name': 'CALYPSO',
      'Sales Process Level': 'Signed EOI and NDA',
    }),

    // Pattern F: same name, different country phones (Etiennette Clamouze)
    row({
      Id: 188,
      'Full Name': 'Etiennette Clamouze',
      'Email Address': 'clamouze.etiennette@gmail.com',
      'Phone Number': '+33767780640',
      'Sales Process Level': 'General Qualified Interest',
    }),
    row({
      Id: 717,
      'Full Name': 'Etiennette Clamouze',
      'Email Address': 'Etiennette@the-manoah.com',
      'Phone Number': '+12645815607',
      'Sales Process Level': 'General Qualified Interest',
    }),

    // Single isolated row to verify non-duplicates pass through
    row({
      Id: 999,
      'Full Name': 'Lone Wolf',
      'Email Address': 'lone@example.com',
      'Phone Number': '+15551234567',
      'Sales Process Level': 'General Qualified Interest',
    }),
  ],
};

describe('transformSnapshot - fixture regression', () => {
  it('produces the expected number of clients + interests', () => {
    const plan = transformSnapshot(FIXTURE);

    // 12 input rows → 7 unique clients (Deepak: 1, Wiarda: 1, Ruiz: 1,
    // Constanzo: 1, Etiennette x2: 2, Lone: 1). Etiennette stays as 2
    // because Pattern F is correctly NOT auto-merged.
    expect(plan.stats.outputClients).toBe(7);
    expect(plan.stats.outputInterests).toBe(12); // one per source row
  });

  it('auto-links every Pattern A–E cluster', () => {
    const plan = transformSnapshot(FIXTURE);
    const linkedSourceIds = new Set<number>();
    for (const link of plan.autoLinks) {
      linkedSourceIds.add(link.leadSourceId);
      for (const merged of link.mergedSourceIds) {
        linkedSourceIds.add(merged);
      }
    }

    // Pattern A: 624 + 625
    expect(linkedSourceIds.has(624) && linkedSourceIds.has(625)).toBe(true);
    // Pattern B: 236 + 536
    expect(linkedSourceIds.has(236) && linkedSourceIds.has(536)).toBe(true);
    // Pattern C: 681 + 682 + 683 (three-way)
    expect(linkedSourceIds.has(681) && linkedSourceIds.has(682) && linkedSourceIds.has(683)).toBe(
      true,
    );
    // Pattern E: 336 + 585
    expect(linkedSourceIds.has(336) && linkedSourceIds.has(585)).toBe(true);
  });

  it('does NOT auto-link Pattern F (Etiennette Clamouze, different country)', () => {
    const plan = transformSnapshot(FIXTURE);
    const linkedSourceIds = new Set<number>();
    for (const link of plan.autoLinks) {
      linkedSourceIds.add(link.leadSourceId);
      for (const merged of link.mergedSourceIds) {
        linkedSourceIds.add(merged);
      }
    }
    // Both Etiennette rows must remain as separate clients.
    expect(linkedSourceIds.has(188)).toBe(false);
    expect(linkedSourceIds.has(717)).toBe(false);
  });

  it('preserves every interest as its own row even when clients merge', () => {
    const plan = transformSnapshot(FIXTURE);
    const sourceIds = plan.interests.map((i) => i.sourceId).sort((a, b) => a - b);
    expect(sourceIds).toEqual([188, 236, 336, 536, 585, 624, 625, 681, 682, 683, 717, 999]);
  });

  it('maps the legacy 8-stage enum to new pipeline stages (§4b signed-off)', () => {
    const plan = transformSnapshot(FIXTURE);
    const stagesById = new Map(plan.interests.map((i) => [i.sourceId, i.pipelineStage]));
    // 681 is "General Qualified Interest" with no berth/size marker → enquiry.
    expect(stagesById.get(681)).toBe('enquiry');
    // 682 is "Specific Qualified Interest" → qualified.
    expect(stagesById.get(682)).toBe('qualified');
    expect(stagesById.get(336)).toBe('contract'); // Contract Signed
    expect(stagesById.get(585)).toBe('eoi'); // Signed EOI and NDA
  });

  it('§4b marker rule: a GQI that named a berth or size is qualified, not a plain enquiry', () => {
    const snap: NocoDbSnapshot = {
      fetchedAt: '2026-06-02T00:00:00.000Z',
      berths: [],
      residentialInterests: [],
      websiteInterestSubmissions: [],
      websiteContactFormSubmissions: [],
      websiteBerthEoiSupplements: [],
      interests: [
        row({
          Id: 9001,
          'Full Name': 'Bare Enquiry',
          'Email Address': 'bare@example.com',
          'Phone Number': '+15550000001',
          'Sales Process Level': 'General Qualified Interest',
        }),
        row({
          Id: 9002,
          'Full Name': 'Named Berth',
          'Email Address': 'berth@example.com',
          'Phone Number': '+15550000002',
          'Sales Process Level': 'General Qualified Interest',
          'Berth Number': 'A1',
        }),
        row({
          Id: 9003,
          'Full Name': 'Wants Size',
          'Email Address': 'size@example.com',
          'Phone Number': '+15550000003',
          'Sales Process Level': 'General Qualified Interest',
          'Berth Size Desired': '20m',
        }),
      ],
    };
    const stagesById = new Map(
      transformSnapshot(snap).interests.map((i) => [i.sourceId, i.pipelineStage]),
    );
    expect(stagesById.get(9001)).toBe('enquiry'); // no marker
    expect(stagesById.get(9002)).toBe('qualified'); // named a berth
    expect(stagesById.get(9003)).toBe('qualified'); // entered a desired size
  });

  it('attaches different yachts to one merged Constanzo client', () => {
    const plan = transformSnapshot(FIXTURE);
    const constanzoClient = plan.clients.find(
      (c) => c.sourceIds.includes(336) && c.sourceIds.includes(585),
    );
    expect(constanzoClient).toBeDefined();
    const yachtsForConstanzo = plan.interests
      .filter((i) => i.clientTempId === constanzoClient!.tempId)
      .map((i) => i.yachtName)
      .sort();
    expect(yachtsForConstanzo).toEqual(['CALYPSO', 'GEMINI']);
  });

  it('produces deterministic output (same input → same plan)', () => {
    // The transform is pure - running it twice should yield bit-identical
    // results. Catches order-dependent bugs in the dedup clustering.
    const a = transformSnapshot(FIXTURE);
    const b = transformSnapshot(FIXTURE);
    expect(JSON.stringify(a.stats)).toBe(JSON.stringify(b.stats));
    expect(a.autoLinks.length).toBe(b.autoLinks.length);
  });
});

// ─── EOI document derivation ───────────────────────────────────────────────

describe('transformSnapshot - EOI document derivation', () => {
  /**
   * A fixture row that mimics a fully-signed legacy interest with a
   * Documenso ID, all three signing slots populated, and an S3 path.
   * The transform should emit one PlannedDocument with three signers.
   */
  function eoiFixture(
    overrides: Partial<NocoDbRow> & { Id: number; documensoID: string },
  ): NocoDbSnapshot {
    return {
      fetchedAt: '2026-05-04T00:00:00.000Z',
      berths: [],
      residentialInterests: [],
      websiteInterestSubmissions: [],
      websiteContactFormSubmissions: [],
      websiteBerthEoiSupplements: [],
      interests: [
        row({
          'Full Name': 'Reza Amjad',
          'Email Address': 'reza@example.com',
          'Phone Number': '+15551112222',
          'Sales Process Level': 'Signed EOI and NDA',
          ...overrides,
        }),
      ],
    };
  }

  it('emits no PlannedDocument when documensoID is absent', () => {
    const plan = transformSnapshot({
      fetchedAt: '2026-05-04T00:00:00.000Z',
      berths: [],
      residentialInterests: [],
      websiteInterestSubmissions: [],
      websiteContactFormSubmissions: [],
      websiteBerthEoiSupplements: [],
      interests: [
        row({
          Id: 100,
          'Full Name': 'No EOI',
          'Email Address': 'no-eoi@example.com',
          'Sales Process Level': 'General Qualified Interest',
        }),
      ],
    });
    expect(plan.documents).toHaveLength(0);
    expect(plan.stats.outputDocuments).toBe(0);
  });

  it('emits one PlannedDocument per interest row with documensoID', () => {
    const plan = transformSnapshot(
      eoiFixture({
        Id: 720,
        documensoID: '107',
        'EOI Status': 'Signed',
        'EOI Time Sent': '2026-04-08T18:07:39.582Z',
        clientSignTime: '2026-04-08T19:00:00.000Z',
        developerSignTime: '2026-04-08T19:23:49.227Z',
        'Signature Link Client': 'https://documenso.example/sign/abc',
        S3_Documenso_Path: 'EOIs/Reza_Amjad_EOI_NDA_signed.pdf',
      }),
    );

    expect(plan.documents).toHaveLength(1);
    expect(plan.stats.outputDocuments).toBe(1);

    const doc = plan.documents[0]!;
    expect(doc.documensoId).toBe('107');
    expect(doc.status).toBe('completed'); // EOI Status = "Signed"
    expect(doc.documentType).toBe('eoi');
    expect(doc.title).toBe('EOI - Reza Amjad');
    expect(doc.notes).toContain('Legacy S3: EOIs/Reza_Amjad_EOI_NDA_signed.pdf');
    expect(doc.notes).toContain('Migrated from legacy NocoDB Interests row #720');

    const clientSigner = doc.signers.find((s) => s.signerRole === 'client');
    const devSigner = doc.signers.find((s) => s.signerRole === 'developer');
    expect(clientSigner?.signerEmail).toBe('reza@example.com');
    expect(clientSigner?.status).toBe('signed');
    expect(clientSigner?.signingUrl).toBe('https://documenso.example/sign/abc');
    expect(devSigner?.status).toBe('signed');
  });

  it('infers status=partially_signed when EOI Status missing but client has signed', () => {
    const plan = transformSnapshot(
      eoiFixture({
        Id: 800,
        documensoID: '200',
        // No EOI Status, no developer sign - only client has signed.
        clientSignTime: '2026-04-01T12:00:00.000Z',
      }),
    );
    expect(plan.documents[0]!.status).toBe('partially_signed');
  });

  it('infers status=sent when EOI Time Sent present but no signatures yet', () => {
    const plan = transformSnapshot(
      eoiFixture({
        Id: 801,
        documensoID: '201',
        'EOI Time Sent': '2026-04-23T03:43:14.593Z',
      }),
    );
    expect(plan.documents[0]!.status).toBe('sent');
  });

  it('preserves the parent interest sourceId on the document so apply can stitch', () => {
    const plan = transformSnapshot(
      eoiFixture({ Id: 720, documensoID: '107', 'EOI Status': 'Signed' }),
    );
    const doc = plan.documents[0]!;
    expect(doc.sourceId).toBe(720);
    expect(doc.clientTempId).toBe('client-720');
  });

  it('skips the CC slot when the legacy row has no CC signature data', () => {
    const plan = transformSnapshot(
      eoiFixture({
        Id: 720,
        documensoID: '107',
        'EOI Status': 'Signed',
        // No Signature Link CC, no ccSignTime, no Embedded CC
      }),
    );
    const ccSigner = plan.documents[0]!.signers.find((s) => s.signerRole === 'cc');
    expect(ccSigner).toBeUndefined();
  });
});

// ─── Residential transform ─────────────────────────────────────────────────

describe('parseFlexibleDate format handling', () => {
  // The legacy NocoDB base mixes ISO datetimes with manual DD-MM-YYYY
  // entries from the Caribbean marina office. parseFlexibleDate handles
  // both. Locking the disambiguation rule with explicit assertions
  // because a regression here would silently shift dates by months,
  // which is exactly the kind of bug nobody notices until much later.
  it('parses unambiguous ISO datetimes verbatim', () => {
    const plan = transformSnapshot({
      fetchedAt: '2026-05-04T00:00:00.000Z',
      berths: [],
      residentialInterests: [],
      websiteInterestSubmissions: [],
      websiteContactFormSubmissions: [],
      websiteBerthEoiSupplements: [],
      interests: [
        row({
          Id: 1,
          'Full Name': 'ISO Test',
          'Email Address': 'iso@example.com',
          'EOI Time Sent': '2026-04-08T18:07:39.582Z',
        }),
      ],
    });
    expect(plan.interests[0]!.dateEoiSent).toBe('2026-04-08T18:07:39.582Z');
  });

  it('parses 01-02-2025 as Feb 1 (DD-MM-YYYY), not Jan 2 (MM-DD-YYYY)', () => {
    // The Caribbean office uses day-first dates. Feb 1 = 01-02-2025.
    // If a regression flips this to MM-DD parsing, the migration would
    // mis-stamp every manually-entered date by ~30 days.
    const plan = transformSnapshot({
      fetchedAt: '2026-05-04T00:00:00.000Z',
      berths: [],
      residentialInterests: [],
      websiteInterestSubmissions: [],
      websiteContactFormSubmissions: [],
      websiteBerthEoiSupplements: [],
      interests: [
        row({
          Id: 2,
          'Full Name': 'DDMM Test',
          'Email Address': 'ddmm@example.com',
          'Time LOI Sent': '01-02-2025',
        }),
      ],
    });
    const iso = plan.interests[0]!.dateContractSent;
    expect(iso?.startsWith('2025-02-01')).toBe(true);
  });
});

describe('transformSnapshot - residential leads', () => {
  it('produces one PlannedResidentialClient per source row', () => {
    const plan = transformSnapshot({
      fetchedAt: '2026-05-04T00:00:00.000Z',
      berths: [],
      websiteInterestSubmissions: [],
      websiteContactFormSubmissions: [],
      websiteBerthEoiSupplements: [],
      interests: [],
      residentialInterests: [
        row({
          Id: 6,
          'Full Name': 'FABIO GOMEZ',
          'Email Address': 'fabio@example.com',
          'Phone Number': '+19143371482',
          'Place of Residence': 'USA',
        }),
        row({
          Id: 7,
          'Full Name': 'James Wilkinson',
          'Email Address': 'jcw@example.com',
          'Phone Number': '+12485684256',
        }),
      ],
    });

    expect(plan.residentialClients).toHaveLength(2);
    expect(plan.stats.outputResidentialClients).toBe(2);

    const fabio = plan.residentialClients.find((c) => c.sourceId === 6);
    expect(fabio?.fullName).toBe('Fabio Gomez');
    expect(fabio?.email).toBe('fabio@example.com');
    expect(fabio?.phoneE164).toBe('+19143371482');
    expect(fabio?.placeOfResidenceCountryIso).toBe('US');
  });

  it('skips residential rows with no name', () => {
    const plan = transformSnapshot({
      fetchedAt: '2026-05-04T00:00:00.000Z',
      berths: [],
      websiteInterestSubmissions: [],
      websiteContactFormSubmissions: [],
      websiteBerthEoiSupplements: [],
      interests: [],
      residentialInterests: [row({ Id: 100, 'Full Name': '', 'Email Address': 'x@y.com' })],
    });

    expect(plan.residentialClients).toHaveLength(0);
    expect(plan.flags.some((f) => f.sourceId === 100)).toBe(true);
  });
});
