import { describe, it, expect } from 'vitest';

import { buildEoiContext } from '@/lib/services/eoi-context';
import { setBerthPriceOverride } from '@/lib/services/interest-berths.service';
import { makePort, makeClient, makeCompany, makeBerth, makeYacht } from '../../helpers/factories';
import { db } from '@/lib/db';
import {
  interests,
  interestBerths,
  clientContacts,
  clientAddresses,
  companyAddresses,
} from '@/lib/db/schema';
import { ValidationError, NotFoundError } from '@/lib/errors';

// ─── Helpers ──────────────────────────────────────────────────────────────────

async function insertInterest(args: {
  portId: string;
  clientId: string;
  yachtId?: string | null;
  berthId?: string | null;
  pipelineStage?: string;
}) {
  const [row] = await db
    .insert(interests)
    .values({
      portId: args.portId,
      clientId: args.clientId,
      yachtId: args.yachtId ?? null,
      pipelineStage: args.pipelineStage ?? 'open',
    })
    .returning();
  // Plan §3.4: legacy interests.berth_id replaced by the junction. Tests
  // that pass berthId materialise a primary junction row alongside the
  // interest so getPrimaryBerth() resolves it during EOI context build.
  if (args.berthId) {
    await db.insert(interestBerths).values({
      interestId: row!.id,
      berthId: args.berthId,
      isPrimary: true,
      isSpecificInterest: true,
    });
  }
  return row!;
}

/**
 * Adds the EOI-required client details (primary email + primary address) so
 * `buildEoiContext` clears its hard gate. Tests that exercise non-EOI-gating
 * behavior should call this once per client they create.
 */
async function seedClientEoiPrereqs(args: {
  clientId: string;
  portId: string;
  email?: string;
  street?: string;
}) {
  await db.insert(clientContacts).values({
    clientId: args.clientId,
    channel: 'email',
    value: args.email ?? `client-${args.clientId.slice(0, 8)}@example.com`,
    isPrimary: true,
  });
  await db.insert(clientAddresses).values({
    clientId: args.clientId,
    portId: args.portId,
    streetAddress: args.street ?? '1 Harbour Way',
    city: 'Anguilla',
    countryIso: 'AI',
    isPrimary: true,
  });
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe('buildEoiContext', () => {
  it('returns a fully-populated context for a client-owned yacht', async () => {
    const port = await makePort();
    const client = await makeClient({
      portId: port.id,
      overrides: { fullName: 'Alice Test', nationalityIso: 'US' },
    });

    // Insert contacts.
    await db.insert(clientContacts).values([
      {
        clientId: client.id,
        channel: 'email',
        value: 'alice@example.com',
        isPrimary: true,
      },
      {
        clientId: client.id,
        channel: 'phone',
        value: '+1-555-1234',
        isPrimary: true,
      },
    ]);

    // Insert primary address.
    await db.insert(clientAddresses).values({
      clientId: client.id,
      portId: port.id,
      streetAddress: '1 Harbour Way',
      city: 'Anguilla',
      countryIso: 'AI',
      isPrimary: true,
    });

    const yacht = await makeYacht({
      portId: port.id,
      ownerType: 'client',
      ownerId: client.id,
      name: 'Sea Breeze',
      overrides: {
        lengthFt: '60',
        widthFt: '20',
        draftFt: '8',
        hullNumber: 'HN-1',
        flag: 'US',
        yearBuilt: 2020,
      },
    });

    const berth = await makeBerth({
      portId: port.id,
      overrides: { mooringNumber: 'M-42', area: 'North', lengthFt: '70' },
    });

    const interest = await insertInterest({
      portId: port.id,
      clientId: client.id,
      yachtId: yacht.id,
      berthId: berth.id,
      pipelineStage: 'in_communication',
    });

    const ctx = await buildEoiContext(interest.id, port.id);

    // Client assertions. Nationality + address country are rendered as
    // localized names (Intl.DisplayNames) from the ISO codes.
    expect(ctx.client.fullName).toBe('Alice Test');
    expect(ctx.client.nationality).toBe('United States');
    expect(ctx.client.primaryEmail).toBe('alice@example.com');
    expect(ctx.client.primaryPhone).toBe('+1-555-1234');
    expect(ctx.client.address).toEqual({
      street: '1 Harbour Way',
      city: 'Anguilla',
      subdivision: '',
      postalCode: '',
      country: 'Anguilla',
      countryIso: 'AI',
    });

    // Yacht assertions.
    expect(ctx.yacht?.name).toBe('Sea Breeze');
    expect(ctx.yacht?.hullNumber).toBe('HN-1');
    expect(ctx.yacht?.yearBuilt).toBe(2020);

    // Berth assertions.
    expect(ctx.berth?.mooringNumber).toBe('M-42');
    expect(ctx.berth?.area).toBe('North');

    // Interest assertions.
    expect(ctx.interest.stage).toBe('in_communication');

    // Port assertions.
    expect(ctx.port.name).toBe(port.name);
    expect(ctx.port.defaultCurrency).toBe(port.defaultCurrency);

    // Date assertions.
    expect(ctx.date.today).toMatch(/^\d{4}-\d{2}-\d{2}$/);
    expect(ctx.date.year).toMatch(/^\d{4}$/);

    // Owner assertions.
    expect(ctx.owner.type).toBe('client');
    expect(ctx.owner.name).toBe('Alice Test');

    // Company field.
    expect(ctx.company).toBeNull();
  });

  it('returns a fully-populated context for a company-owned yacht', async () => {
    const port = await makePort();
    const company = await makeCompany({
      portId: port.id,
      overrides: { name: 'Acme Shipping', legalName: 'Acme Shipping Ltd.' },
    });
    const client = await makeClient({
      portId: port.id,
      overrides: { fullName: 'Bob Contact' },
    });
    await seedClientEoiPrereqs({ clientId: client.id, portId: port.id });

    const yacht = await makeYacht({
      portId: port.id,
      ownerType: 'company',
      ownerId: company.id,
      name: 'Acme Runner',
    });

    const berth = await makeBerth({ portId: port.id });

    const interest = await insertInterest({
      portId: port.id,
      clientId: client.id,
      yachtId: yacht.id,
      berthId: berth.id,
    });

    const ctx = await buildEoiContext(interest.id, port.id);

    expect(ctx.owner.type).toBe('company');
    expect(ctx.owner.name).toBe('Acme Shipping');
    expect(ctx.owner.legalName).toBe('Acme Shipping Ltd.');
    expect(ctx.company).not.toBeNull();
    expect(ctx.company!.name).toBe('Acme Shipping');
    expect(ctx.company!.legalName).toBe('Acme Shipping Ltd.');
  });

  it('includes company billingAddress when company has a primary address', async () => {
    const port = await makePort();
    const company = await makeCompany({
      portId: port.id,
      overrides: { name: 'Billing Co' },
    });
    await db.insert(companyAddresses).values({
      companyId: company.id,
      portId: port.id,
      streetAddress: '99 Commerce St',
      city: 'Valley',
      countryIso: 'AI',
      isPrimary: true,
    });

    const client = await makeClient({ portId: port.id });
    await seedClientEoiPrereqs({ clientId: client.id, portId: port.id });
    const yacht = await makeYacht({
      portId: port.id,
      ownerType: 'company',
      ownerId: company.id,
    });
    const berth = await makeBerth({ portId: port.id });

    const interest = await insertInterest({
      portId: port.id,
      clientId: client.id,
      yachtId: yacht.id,
      berthId: berth.id,
    });

    const ctx = await buildEoiContext(interest.id, port.id);

    expect(ctx.company).not.toBeNull();
    expect(ctx.company!.billingAddress).not.toBeNull();
    expect(ctx.company!.billingAddress).toContain('99 Commerce St');
    expect(ctx.company!.billingAddress).toContain('Valley');
    // Country is rendered as the localized name (AI -> Anguilla).
    expect(ctx.company!.billingAddress).toContain('Anguilla');
  });

  it('builds a valid context when yacht is missing (Section 3 left blank)', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });
    await seedClientEoiPrereqs({ clientId: client.id, portId: port.id });
    const berth = await makeBerth({ portId: port.id });

    const interest = await insertInterest({
      portId: port.id,
      clientId: client.id,
      yachtId: null,
      berthId: berth.id,
    });

    const ctx = await buildEoiContext(interest.id, port.id);
    expect(ctx.yacht).toBeNull();
    expect(ctx.berth?.mooringNumber).toBe(berth.mooringNumber);
    // Owner falls back to the interest's client when no yacht is linked.
    expect(ctx.owner.type).toBe('client');
    expect(ctx.owner.name).toBe(client.fullName);
  });

  it('builds a valid context when berth is missing (Section 3 left blank)', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });
    await seedClientEoiPrereqs({ clientId: client.id, portId: port.id });
    const yacht = await makeYacht({
      portId: port.id,
      ownerType: 'client',
      ownerId: client.id,
    });

    const interest = await insertInterest({
      portId: port.id,
      clientId: client.id,
      yachtId: yacht.id,
      berthId: null,
    });

    const ctx = await buildEoiContext(interest.id, port.id);
    expect(ctx.berth).toBeNull();
    expect(ctx.yacht?.name).toBe(yacht.name);
  });

  it('throws ValidationError when client has no email', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });
    // Address only, no email - gate should fail.
    await db.insert(clientAddresses).values({
      clientId: client.id,
      portId: port.id,
      streetAddress: '1 Harbour Way',
      city: 'Anguilla',
      countryIso: 'AI',
      isPrimary: true,
    });

    const interest = await insertInterest({ portId: port.id, clientId: client.id });

    await expect(buildEoiContext(interest.id, port.id)).rejects.toThrow(ValidationError);
    await expect(buildEoiContext(interest.id, port.id)).rejects.toThrow(/client email/i);
  });

  it('throws ValidationError when client has no primary address', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });
    // Email only, no address - gate should fail.
    await db.insert(clientContacts).values({
      clientId: client.id,
      channel: 'email',
      value: 'test@example.com',
      isPrimary: true,
    });

    const interest = await insertInterest({ portId: port.id, clientId: client.id });

    await expect(buildEoiContext(interest.id, port.id)).rejects.toThrow(ValidationError);
    await expect(buildEoiContext(interest.id, port.id)).rejects.toThrow(/client address/i);
  });

  it('renders the deal-specific override price for the primary berth', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });
    await seedClientEoiPrereqs({ clientId: client.id, portId: port.id });
    const berth = await makeBerth({
      portId: port.id,
      overrides: { mooringNumber: 'P-1', price: '3880800', priceCurrency: 'USD' },
    });
    const interest = await insertInterest({
      portId: port.id,
      clientId: client.id,
      berthId: berth.id,
    });

    // Base list price flows through first.
    let ctx = await buildEoiContext(interest.id, port.id);
    expect(ctx.berth?.price).toBe('3880800');
    expect(ctx.berth?.priceCurrency).toBe('USD');

    // The deal-specific override supersedes the list price for this interest.
    await setBerthPriceOverride(interest.id, berth.id, 1500000, 'EUR', port.id);
    ctx = await buildEoiContext(interest.id, port.id);
    expect(ctx.berth?.price).toBe('1500000');
    expect(ctx.berth?.priceCurrency).toBe('EUR');
  });

  it('throws NotFoundError for non-existent interest', async () => {
    const port = await makePort();
    await expect(buildEoiContext('fake-id', port.id)).rejects.toThrow(NotFoundError);
  });

  it('is tenant-scoped (interest from different port throws NotFoundError)', async () => {
    const portA = await makePort();
    const portB = await makePort();
    const client = await makeClient({ portId: portA.id });
    const yacht = await makeYacht({
      portId: portA.id,
      ownerType: 'client',
      ownerId: client.id,
    });
    const berth = await makeBerth({ portId: portA.id });

    const interest = await insertInterest({
      portId: portA.id,
      clientId: client.id,
      yachtId: yacht.id,
      berthId: berth.id,
    });

    await expect(buildEoiContext(interest.id, portB.id)).rejects.toThrow(NotFoundError);
  });
});
