import { describe, it, expect } from 'vitest';
import {
  addMembership,
  updateMembership,
  endMembership,
  setPrimary,
  listByCompany,
  listByClient,
} from '@/lib/services/company-memberships.service';
import { makeCompany, makeClient, makePort, makeAuditMeta } from '../../helpers/factories';
import { db } from '@/lib/db';
import { companyMemberships } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';

describe('company-memberships.service - addMembership', () => {
  it('creates a membership for a valid client + company in the same port', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });
    const client = await makeClient({ portId: port.id });

    const membership = await addMembership(
      company.id,
      port.id,
      {
        clientId: client.id,
        role: 'director',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );

    expect(membership.id).toBeTruthy();
    expect(membership.companyId).toBe(company.id);
    expect(membership.clientId).toBe(client.id);
    expect(membership.role).toBe('director');
    expect(membership.endDate).toBeNull();
  });

  it('throws ValidationError when company not found', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });

    await expect(
      addMembership(
        'nonexistent-company',
        port.id,
        {
          clientId: client.id,
          role: 'director',
          startDate: new Date('2026-01-01'),
          isPrimary: false,
        },
        makeAuditMeta({ portId: port.id }),
      ),
    ).rejects.toBeInstanceOf(ValidationError);
  });

  it('throws ValidationError when client not found', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });

    await expect(
      addMembership(
        company.id,
        port.id,
        {
          clientId: 'nonexistent-client',
          role: 'director',
          startDate: new Date('2026-01-01'),
          isPrimary: false,
        },
        makeAuditMeta({ portId: port.id }),
      ),
    ).rejects.toBeInstanceOf(ValidationError);
  });

  it('throws ValidationError when client is in a different port (cross-tenant)', async () => {
    const portA = await makePort();
    const portB = await makePort();
    const company = await makeCompany({ portId: portA.id });
    const clientInB = await makeClient({ portId: portB.id });

    await expect(
      addMembership(
        company.id,
        portA.id,
        {
          clientId: clientInB.id,
          role: 'director',
          startDate: new Date('2026-01-01'),
          isPrimary: false,
        },
        makeAuditMeta({ portId: portA.id }),
      ),
    ).rejects.toBeInstanceOf(ValidationError);
  });

  it('throws ConflictError when exact duplicate (companyId + clientId + role + startDate)', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });
    const client = await makeClient({ portId: port.id });

    const startDate = new Date('2026-01-01');

    await addMembership(
      company.id,
      port.id,
      { clientId: client.id, role: 'director', startDate, isPrimary: false },
      makeAuditMeta({ portId: port.id }),
    );

    await expect(
      addMembership(
        company.id,
        port.id,
        { clientId: client.id, role: 'director', startDate, isPrimary: false },
        makeAuditMeta({ portId: port.id }),
      ),
    ).rejects.toBeInstanceOf(ConflictError);
  });
});

describe('company-memberships.service - updateMembership', () => {
  it('updates fields', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });
    const client = await makeClient({ portId: port.id });

    const membership = await addMembership(
      company.id,
      port.id,
      {
        clientId: client.id,
        role: 'officer',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );

    const updated = await updateMembership(
      membership.id,
      port.id,
      { role: 'director', notes: 'Promoted' },
      makeAuditMeta({ portId: port.id }),
    );

    expect(updated.role).toBe('director');
    expect(updated.notes).toBe('Promoted');
  });

  it('throws NotFoundError for cross-tenant', async () => {
    const portA = await makePort();
    const portB = await makePort();
    const company = await makeCompany({ portId: portB.id });
    const client = await makeClient({ portId: portB.id });

    const membership = await addMembership(
      company.id,
      portB.id,
      {
        clientId: client.id,
        role: 'director',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: portB.id }),
    );

    await expect(
      updateMembership(
        membership.id,
        portA.id,
        { notes: 'Hijack' },
        makeAuditMeta({ portId: portA.id }),
      ),
    ).rejects.toBeInstanceOf(NotFoundError);
  });
});

describe('company-memberships.service - setPrimary', () => {
  it('sets only one membership as primary per company (atomic un-primary others)', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });
    const clientA = await makeClient({ portId: port.id });
    const clientB = await makeClient({ portId: port.id });
    const clientC = await makeClient({ portId: port.id });

    const m1 = await addMembership(
      company.id,
      port.id,
      {
        clientId: clientA.id,
        role: 'director',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );
    const m2 = await addMembership(
      company.id,
      port.id,
      {
        clientId: clientB.id,
        role: 'officer',
        startDate: new Date('2026-02-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );
    const m3 = await addMembership(
      company.id,
      port.id,
      {
        clientId: clientC.id,
        role: 'broker',
        startDate: new Date('2026-03-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );

    // Mark all primary via the service - only the last call should leave a
    // single primary survivor (m3).
    await setPrimary(m1.id, port.id, makeAuditMeta({ portId: port.id }));
    await setPrimary(m2.id, port.id, makeAuditMeta({ portId: port.id }));
    await setPrimary(m3.id, port.id, makeAuditMeta({ portId: port.id }));

    const rows = await db
      .select()
      .from(companyMemberships)
      .where(eq(companyMemberships.companyId, company.id));

    const primaryRows = rows.filter((r) => r.isPrimary);
    expect(primaryRows).toHaveLength(1);
    expect(primaryRows[0]!.id).toBe(m3.id);
  });

  it('throws NotFoundError for cross-tenant membership', async () => {
    const portA = await makePort();
    const portB = await makePort();
    const company = await makeCompany({ portId: portB.id });
    const client = await makeClient({ portId: portB.id });

    const membership = await addMembership(
      company.id,
      portB.id,
      {
        clientId: client.id,
        role: 'director',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: portB.id }),
    );

    await expect(
      setPrimary(membership.id, portA.id, makeAuditMeta({ portId: portA.id })),
    ).rejects.toBeInstanceOf(NotFoundError);
  });
});

describe('company-memberships.service - endMembership', () => {
  it('sets endDate', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });
    const client = await makeClient({ portId: port.id });

    const membership = await addMembership(
      company.id,
      port.id,
      {
        clientId: client.id,
        role: 'director',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );

    const endDate = new Date('2026-06-30');
    const ended = await endMembership(
      membership.id,
      port.id,
      { endDate },
      makeAuditMeta({ portId: port.id }),
    );

    expect(ended.endDate).not.toBeNull();
    expect(ended.endDate!.getTime()).toBe(endDate.getTime());
  });
});

describe('company-memberships.service - listByCompany / listByClient', () => {
  it('returns active memberships only by default', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });
    const clientA = await makeClient({ portId: port.id });
    const clientB = await makeClient({ portId: port.id });

    const active = await addMembership(
      company.id,
      port.id,
      {
        clientId: clientA.id,
        role: 'director',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );
    const endedMembership = await addMembership(
      company.id,
      port.id,
      {
        clientId: clientB.id,
        role: 'officer',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );
    await endMembership(
      endedMembership.id,
      port.id,
      { endDate: new Date('2026-03-01') },
      makeAuditMeta({ portId: port.id }),
    );

    const results = await listByCompany(company.id, port.id);
    const ids = results.map((m) => m.id);
    expect(ids).toContain(active.id);
    expect(ids).not.toContain(endedMembership.id);
  });

  it('includes ended memberships when activeOnly=false', async () => {
    const port = await makePort();
    const company = await makeCompany({ portId: port.id });
    const client = await makeClient({ portId: port.id });

    const membership = await addMembership(
      company.id,
      port.id,
      {
        clientId: client.id,
        role: 'director',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );
    await endMembership(
      membership.id,
      port.id,
      { endDate: new Date('2026-02-01') },
      makeAuditMeta({ portId: port.id }),
    );

    const resultsActive = await listByCompany(company.id, port.id);
    expect(resultsActive.map((m) => m.id)).not.toContain(membership.id);

    const resultsAll = await listByCompany(company.id, port.id, { activeOnly: false });
    expect(resultsAll.map((m) => m.id)).toContain(membership.id);
  });

  it('is tenant-scoped (listByCompany throws NotFoundError for cross-tenant company)', async () => {
    const portA = await makePort();
    const portB = await makePort();
    const companyInB = await makeCompany({ portId: portB.id });

    await expect(listByCompany(companyInB.id, portA.id)).rejects.toBeInstanceOf(NotFoundError);
  });

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

    await expect(listByClient(clientInB.id, portA.id)).rejects.toBeInstanceOf(NotFoundError);
  });

  it('listByClient returns active memberships only by default', async () => {
    const port = await makePort();
    const companyA = await makeCompany({ portId: port.id });
    const companyB = await makeCompany({ portId: port.id });
    const client = await makeClient({ portId: port.id });

    const active = await addMembership(
      companyA.id,
      port.id,
      {
        clientId: client.id,
        role: 'director',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );
    const endedMembership = await addMembership(
      companyB.id,
      port.id,
      {
        clientId: client.id,
        role: 'officer',
        startDate: new Date('2026-01-01'),
        isPrimary: false,
      },
      makeAuditMeta({ portId: port.id }),
    );
    await endMembership(
      endedMembership.id,
      port.id,
      { endDate: new Date('2026-03-01') },
      makeAuditMeta({ portId: port.id }),
    );

    const results = await listByClient(client.id, port.id);
    const ids = results.map((m) => m.id);
    expect(ids).toContain(active.id);
    expect(ids).not.toContain(endedMembership.id);
  });
});
