import { describe, it, expect, vi, beforeAll } from 'vitest';
import { and, eq, isNull } from 'drizzle-orm';

import { db } from '@/lib/db';
import { clients, clientContacts } from '@/lib/db/schema/clients';
import { yachts, yachtOwnershipHistory } from '@/lib/db/schema/yachts';
import { companies, companyMemberships } from '@/lib/db/schema/companies';
import { interests } from '@/lib/db/schema/interests';
import { makePort } from '../helpers/factories';
import { makeMockRequest } from '../helpers/route-tester';

// Mock fire-and-forget side-effects so the test doesn't hit Redis / external services.
vi.mock('@/lib/socket/server', () => ({ emitToRoom: vi.fn() }));
vi.mock('@/lib/queue', () => ({
  getQueue: () => ({ add: vi.fn().mockResolvedValue(undefined) }),
}));
vi.mock('@/lib/services/inquiry-notifications.service', () => ({
  sendInquiryNotifications: vi.fn().mockResolvedValue(undefined),
}));

// The rate-limiter is keyed by IP header and is now redis-backed; entries
// pexpire after the publicForm window (1h). Randomize the high octets so a
// fresh test run doesn't collide with leftover redis state from a previous
// run sharing the same redis instance.
const IP_PREFIX = `10.${Math.floor(Math.random() * 200) + 10}`;
let ipCounter = 1;
function uniqueIp(): string {
  ipCounter += 1;
  return `${IP_PREFIX}.${Math.floor(ipCounter / 255) % 255}.${ipCounter % 255}`;
}

describe('POST /api/public/interests - trio creation', () => {
  let POST: typeof import('@/app/api/public/interests/route').POST;

  beforeAll(async () => {
    // Import after mocks are registered.
    const mod = await import('@/app/api/public/interests/route');
    POST = mod.POST;
  });

  it('creates client + yacht + interest atomically', async () => {
    const port = await makePort();
    const email = `trio-client-${Math.random().toString(36).slice(2, 8)}@test.local`;

    const req = makeMockRequest('POST', `http://localhost/api/public/interests?portId=${port.id}`, {
      headers: { 'x-forwarded-for': uniqueIp() },
      body: {
        firstName: 'Alice',
        lastName: 'Mariner',
        email,
        phone: '+10000000001',
        yacht: {
          name: 'Sea Star',
          lengthFt: 52,
          widthFt: 14,
          draftFt: 6,
        },
      },
    });
    const res = await POST(req);
    expect(res.status).toBe(201);
    const body = (await res.json()) as any;
    const interestId: string = body.data.id;

    const [interest] = await db.select().from(interests).where(eq(interests.id, interestId));
    expect(interest).toBeDefined();
    expect(interest!.portId).toBe(port.id);
    expect(interest!.pipelineStage).toBe('enquiry');
    expect(interest!.yachtId).not.toBeNull();
    expect(interest!.clientId).not.toBeNull();

    // Yacht exists, owned by the client
    const [yacht] = await db.select().from(yachts).where(eq(yachts.id, interest!.yachtId!));
    expect(yacht).toBeDefined();
    expect(yacht!.name).toBe('Sea Star');
    expect(yacht!.currentOwnerType).toBe('client');
    expect(yacht!.currentOwnerId).toBe(interest!.clientId);

    // Ownership history row created
    const historyRows = await db
      .select()
      .from(yachtOwnershipHistory)
      .where(eq(yachtOwnershipHistory.yachtId, yacht!.id));
    expect(historyRows.length).toBe(1);
    expect(historyRows[0]!.endDate).toBeNull();
    expect(historyRows[0]!.ownerType).toBe('client');
    expect(historyRows[0]!.ownerId).toBe(interest!.clientId);

    // Client has email + phone contacts
    const contacts = await db
      .select()
      .from(clientContacts)
      .where(eq(clientContacts.clientId, interest!.clientId));
    expect(contacts.some((c) => c.channel === 'email' && c.value === email)).toBe(true);
    expect(contacts.some((c) => c.channel === 'phone' && c.value === '+10000000001')).toBe(true);
  });

  it('creates client + company + membership + company-owned yacht + interest when company provided', async () => {
    const port = await makePort();
    const email = `trio-co-${Math.random().toString(36).slice(2, 8)}@test.local`;
    const companyName = `Nautical Holdings ${Math.random().toString(36).slice(2, 8)}`;

    const req = makeMockRequest('POST', `http://localhost/api/public/interests?portId=${port.id}`, {
      headers: { 'x-forwarded-for': uniqueIp() },
      body: {
        firstName: 'Bob',
        lastName: 'Director',
        email,
        phone: '+10000000002',
        yacht: { name: 'Corporate Cruiser', lengthFt: 80 },
        company: {
          name: companyName,
          role: 'director',
        },
      },
    });
    const res = await POST(req);
    expect(res.status).toBe(201);
    const body = (await res.json()) as any;
    const interestId: string = body.data.id;

    const [interest] = await db.select().from(interests).where(eq(interests.id, interestId));
    expect(interest).toBeDefined();
    expect(interest!.yachtId).not.toBeNull();

    // Yacht owned by the company
    const [yacht] = await db.select().from(yachts).where(eq(yachts.id, interest!.yachtId!));
    expect(yacht!.currentOwnerType).toBe('company');

    // Company exists and matches
    const [company] = await db
      .select()
      .from(companies)
      .where(eq(companies.id, yacht!.currentOwnerId));
    expect(company!.name).toBe(companyName);
    expect(company!.portId).toBe(port.id);

    // Ownership-history points at the company
    const historyRows = await db
      .select()
      .from(yachtOwnershipHistory)
      .where(eq(yachtOwnershipHistory.yachtId, yacht!.id));
    expect(historyRows.length).toBe(1);
    expect(historyRows[0]!.ownerType).toBe('company');
    expect(historyRows[0]!.ownerId).toBe(company!.id);

    // Active membership linking client -> company
    const memberships = await db
      .select()
      .from(companyMemberships)
      .where(
        and(
          eq(companyMemberships.companyId, company!.id),
          eq(companyMemberships.clientId, interest!.clientId),
          isNull(companyMemberships.endDate),
        ),
      );
    expect(memberships.length).toBe(1);
    expect(memberships[0]!.role).toBe('director');
  });

  it('reuses existing client when email matches (same port)', async () => {
    const port = await makePort();
    const email = `trio-reuse-${Math.random().toString(36).slice(2, 8)}@test.local`;

    const firstReq = makeMockRequest(
      'POST',
      `http://localhost/api/public/interests?portId=${port.id}`,
      {
        headers: { 'x-forwarded-for': uniqueIp() },
        body: {
          firstName: 'Carol',
          lastName: 'Returning',
          email,
          phone: '+10000000003',
          yacht: { name: 'First Boat' },
        },
      },
    );
    const firstRes = await POST(firstReq);
    expect(firstRes.status).toBe(201);
    const firstBody = (await firstRes.json()) as any;

    const [firstInterest] = await db
      .select()
      .from(interests)
      .where(eq(interests.id, firstBody.data.id));
    const originalClientId = firstInterest!.clientId;

    // Second submission with the same email
    const secondReq = makeMockRequest(
      'POST',
      `http://localhost/api/public/interests?portId=${port.id}`,
      {
        headers: { 'x-forwarded-for': uniqueIp() },
        body: {
          firstName: 'Carol',
          lastName: 'Returning',
          email,
          phone: '+10000000003',
          yacht: { name: 'Second Boat' },
        },
      },
    );
    const secondRes = await POST(secondReq);
    expect(secondRes.status).toBe(201);
    const secondBody = (await secondRes.json()) as any;

    const [secondInterest] = await db
      .select()
      .from(interests)
      .where(eq(interests.id, secondBody.data.id));
    expect(secondInterest!.clientId).toBe(originalClientId);

    // A second yacht row was created (not deduped) - each submission is its
    // own inquiry about a possibly-different yacht.
    const clientsMatching = await db.select().from(clients).where(eq(clients.id, originalClientId));
    expect(clientsMatching.length).toBe(1);

    const [secondYacht] = await db
      .select()
      .from(yachts)
      .where(eq(yachts.id, secondInterest!.yachtId!));
    expect(secondYacht!.name).toBe('Second Boat');
    expect(secondYacht!.id).not.toBe(firstInterest!.yachtId);
  });

  it('reuses existing company when name matches case-insensitively (same port)', async () => {
    const port = await makePort();
    const email1 = `trio-coreuse1-${Math.random().toString(36).slice(2, 8)}@test.local`;
    const email2 = `trio-coreuse2-${Math.random().toString(36).slice(2, 8)}@test.local`;
    const companyName = `Harbor Partners ${Math.random().toString(36).slice(2, 8)}`;

    const firstReq = makeMockRequest(
      'POST',
      `http://localhost/api/public/interests?portId=${port.id}`,
      {
        headers: { 'x-forwarded-for': uniqueIp() },
        body: {
          firstName: 'Dana',
          lastName: 'Founder',
          email: email1,
          phone: '+10000000004',
          yacht: { name: 'Flagship' },
          company: { name: companyName, role: 'director' },
        },
      },
    );
    const firstRes = await POST(firstReq);
    expect(firstRes.status).toBe(201);
    const firstBody = (await firstRes.json()) as any;
    const [firstInterest] = await db
      .select()
      .from(interests)
      .where(eq(interests.id, firstBody.data.id));
    const [firstYacht] = await db
      .select()
      .from(yachts)
      .where(eq(yachts.id, firstInterest!.yachtId!));
    const originalCompanyId = firstYacht!.currentOwnerId;

    // Second submission - same company name, different casing, different client
    const secondReq = makeMockRequest(
      'POST',
      `http://localhost/api/public/interests?portId=${port.id}`,
      {
        headers: { 'x-forwarded-for': uniqueIp() },
        body: {
          firstName: 'Evan',
          lastName: 'Employee',
          email: email2,
          phone: '+10000000005',
          yacht: { name: 'Second Flagship' },
          company: { name: companyName.toUpperCase(), role: 'employee' },
        },
      },
    );
    const secondRes = await POST(secondReq);
    expect(secondRes.status).toBe(201);
    const secondBody = (await secondRes.json()) as any;
    const [secondInterest] = await db
      .select()
      .from(interests)
      .where(eq(interests.id, secondBody.data.id));
    const [secondYacht] = await db
      .select()
      .from(yachts)
      .where(eq(yachts.id, secondInterest!.yachtId!));
    expect(secondYacht!.currentOwnerId).toBe(originalCompanyId);

    // Only one company row exists for that (portId, lowered name)
    const allCompanies = await db.select().from(companies).where(eq(companies.portId, port.id));
    const matching = allCompanies.filter((c) => c.name.toLowerCase() === companyName.toLowerCase());
    expect(matching.length).toBe(1);

    // Second client has its own membership in the same company
    const memberships = await db
      .select()
      .from(companyMemberships)
      .where(
        and(
          eq(companyMemberships.companyId, originalCompanyId),
          eq(companyMemberships.clientId, secondInterest!.clientId),
          isNull(companyMemberships.endDate),
        ),
      );
    expect(memberships.length).toBe(1);
    expect(memberships[0]!.role).toBe('employee');
  });
});
