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

import { db } from '@/lib/db';
import { clientAddresses, companyAddresses } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import {
  listClientAddresses,
  addClientAddress,
  updateClientAddress,
  removeClientAddress,
} from '@/lib/services/clients.service';
import {
  listCompanyAddresses,
  addCompanyAddress,
  updateCompanyAddress,
  removeCompanyAddress,
} from '@/lib/services/companies.service';
import { NotFoundError } from '@/lib/errors';
import { makePort, makeClient, makeCompany } from '../helpers/factories';

const META = (portId: string) => ({
  userId: 'test-user',
  portId,
  ipAddress: '127.0.0.1',
  userAgent: 'vitest',
});

describe('client addresses service', () => {
  it('adds, lists, updates, and removes a client address', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });

    // Initially empty.
    const empty = await listClientAddresses(client.id, port.id);
    expect(empty).toHaveLength(0);

    const added = await addClientAddress(
      client.id,
      port.id,
      {
        label: 'Home',
        streetAddress: '1 Pier Rd',
        city: 'Marbella',
        countryIso: 'ES',
        subdivisionIso: 'ES-MA',
        postalCode: '29602',
        isPrimary: true,
      },
      META(port.id),
    );

    expect(added.label).toBe('Home');
    expect(added.countryIso).toBe('ES');
    expect(added.isPrimary).toBe(true);

    const list = await listClientAddresses(client.id, port.id);
    expect(list).toHaveLength(1);

    const updated = await updateClientAddress(
      added.id,
      client.id,
      port.id,
      { city: 'Málaga' },
      META(port.id),
    );
    expect(updated.city).toBe('Málaga');

    await removeClientAddress(added.id, client.id, port.id, META(port.id));

    const after = await listClientAddresses(client.id, port.id);
    expect(after).toHaveLength(0);
  });

  it('demotes an existing primary when adding a new primary', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });

    const first = await addClientAddress(
      client.id,
      port.id,
      { label: 'Home', isPrimary: true },
      META(port.id),
    );

    const second = await addClientAddress(
      client.id,
      port.id,
      { label: 'Office', isPrimary: true },
      META(port.id),
    );

    const rows = await db.query.clientAddresses.findMany({
      where: eq(clientAddresses.clientId, client.id),
    });
    const primaries = rows.filter((r) => r.isPrimary);
    expect(primaries).toHaveLength(1);
    expect(primaries[0]!.id).toBe(second.id);

    // The previously-primary row is now demoted, not deleted.
    const firstAfter = rows.find((r) => r.id === first.id);
    expect(firstAfter?.isPrimary).toBe(false);
  });

  it('demotes other primaries when patching to primary=true', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });

    const first = await addClientAddress(
      client.id,
      port.id,
      { label: 'Home', isPrimary: true },
      META(port.id),
    );
    const second = await addClientAddress(
      client.id,
      port.id,
      { label: 'Office', isPrimary: false },
      META(port.id),
    );

    await updateClientAddress(second.id, client.id, port.id, { isPrimary: true }, META(port.id));

    const rows = await db.query.clientAddresses.findMany({
      where: eq(clientAddresses.clientId, client.id),
    });
    const primary = rows.find((r) => r.isPrimary);
    expect(primary?.id).toBe(second.id);
    expect(rows.find((r) => r.id === first.id)?.isPrimary).toBe(false);
  });

  it('is tenant-scoped (cross-port access throws NotFoundError)', async () => {
    const portA = await makePort();
    const portB = await makePort();
    const client = await makeClient({ portId: portA.id });

    await expect(listClientAddresses(client.id, portB.id)).rejects.toThrow(NotFoundError);
    await expect(
      addClientAddress(client.id, portB.id, { label: 'X' }, META(portB.id)),
    ).rejects.toThrow(NotFoundError);
  });
});

describe('company addresses service', () => {
  it('adds, lists, updates, and removes a company address', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });

    const added = await addCompanyAddress(
      company.id,
      port.id,
      { label: 'HQ', countryIso: 'GB', isPrimary: true },
      META(port.id),
    );
    expect(added.countryIso).toBe('GB');

    const list = await listCompanyAddresses(company.id, port.id);
    expect(list).toHaveLength(1);

    const updated = await updateCompanyAddress(
      added.id,
      company.id,
      port.id,
      { city: 'London' },
      META(port.id),
    );
    expect(updated.city).toBe('London');

    await removeCompanyAddress(added.id, company.id, port.id, META(port.id));
    const after = await db.query.companyAddresses.findMany({
      where: eq(companyAddresses.companyId, company.id),
    });
    expect(after).toHaveLength(0);
  });

  it('demotes an existing primary when adding a new primary', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });

    await addCompanyAddress(company.id, port.id, { label: 'HQ', isPrimary: true }, META(port.id));
    const second = await addCompanyAddress(
      company.id,
      port.id,
      { label: 'Branch', isPrimary: true },
      META(port.id),
    );

    const rows = await db.query.companyAddresses.findMany({
      where: eq(companyAddresses.companyId, company.id),
    });
    const primaries = rows.filter((r) => r.isPrimary);
    expect(primaries).toHaveLength(1);
    expect(primaries[0]!.id).toBe(second.id);
  });
});
