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

import { db } from '@/lib/db';
import {
  clientAddresses,
  clientContacts,
  clientNotes,
  clientTags,
  tags,
  gdprExports,
} from '@/lib/db/schema';
import { user } from '@/lib/db/schema/users';
import { buildClientBundle, renderBundleHtml } from '@/lib/services/gdpr-bundle-builder';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { makePort, makeClient, makeYacht } from '../helpers/factories';

let TEST_USER_ID = '';

beforeAll(async () => {
  // Pull any existing user - gdpr_exports.requested_by has an FK that needs
  // to resolve. Tests don't need the user to be specific; they just need it
  // to exist.
  const [u] = await db.select({ id: user.id }).from(user).limit(1);
  if (!u) {
    throw new Error('No user available; run pnpm db:seed first');
  }
  TEST_USER_ID = u.id;
});

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

describe('buildClientBundle', () => {
  it('aggregates client + contacts + addresses + tags + yachts', async () => {
    const port = await makePort();
    const client = await makeClient({
      portId: port.id,
      overrides: { fullName: 'Alice Test', nationalityIso: 'GB' },
    });

    await db.insert(clientContacts).values({
      clientId: client.id,
      channel: 'email',
      value: 'alice@example.com',
      isPrimary: true,
    });
    await db.insert(clientAddresses).values({
      clientId: client.id,
      portId: port.id,
      label: 'Home',
      streetAddress: '1 Pier Way',
      countryIso: 'GB',
      isPrimary: true,
    });
    await db.insert(clientNotes).values({
      clientId: client.id,
      authorId: 'tester',
      content: 'Met at boat show',
    });
    const [tagRow] = await db
      .insert(tags)
      .values({ portId: port.id, name: 'VIP', color: '#ff0000' })
      .returning();
    await db.insert(clientTags).values({ clientId: client.id, tagId: tagRow!.id });
    await makeYacht({ portId: port.id, ownerType: 'client', ownerId: client.id });

    const bundle = await buildClientBundle(client.id, port.id);

    expect(bundle.client.fullName).toBe('Alice Test');
    expect(bundle.contacts).toHaveLength(1);
    expect(bundle.addresses).toHaveLength(1);
    expect(bundle.notes).toHaveLength(1);
    expect(bundle.tags).toHaveLength(1);
    expect(bundle.tags[0]?.name).toBe('VIP');
    expect(bundle.ownedYachts).toHaveLength(1);
    expect(bundle.meta.clientId).toBe(client.id);
    expect(bundle.meta.portId).toBe(port.id);
    expect(bundle.meta.schemaVersion).toBe(1);
  });

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

    await expect(buildClientBundle(client.id, portB.id)).rejects.toThrow(NotFoundError);
  });

  it('returns empty arrays when the client has no related rows', async () => {
    const port = await makePort();
    const client = await makeClient({ portId: port.id });

    const bundle = await buildClientBundle(client.id, port.id);
    expect(bundle.contacts).toEqual([]);
    expect(bundle.addresses).toEqual([]);
    expect(bundle.tags).toEqual([]);
    expect(bundle.notes).toEqual([]);
    expect(bundle.ownedYachts).toEqual([]);
    expect(bundle.companyMemberships).toEqual([]);
    expect(bundle.invoices).toEqual([]);
  });
});

describe('renderBundleHtml', () => {
  it('produces a self-contained HTML doc with section headings', async () => {
    const port = await makePort();
    const client = await makeClient({
      portId: port.id,
      overrides: { fullName: 'Render Test' },
    });
    const bundle = await buildClientBundle(client.id, port.id);
    const html = renderBundleHtml(bundle);
    expect(html.startsWith('<!doctype html>')).toBe(true);
    expect(html).toContain('Render Test');
    expect(html).toContain('Personal data export');
    expect(html).toContain('Contacts');
    expect(html).toContain('Addresses');
    expect(html).toContain('Audit trail');
    // No external requests.
    expect(html).not.toMatch(/https?:\/\/[^"'\s]+\.(?:js|css)/);
  });

  it('escapes HTML in client field values to prevent injection', async () => {
    const port = await makePort();
    const client = await makeClient({
      portId: port.id,
      overrides: { fullName: '<script>alert(1)</script>' },
    });
    const bundle = await buildClientBundle(client.id, port.id);
    const html = renderBundleHtml(bundle);
    // The literal "<script>" must not appear unescaped anywhere in the output.
    expect(html).not.toContain('<script>alert(1)</script>');
    expect(html).toContain('&lt;script&gt;');
  });
});

describe('requestGdprExport', () => {
  it('creates a pending row and queues a job', async () => {
    // Stub the BullMQ queue so we don't actually push jobs to Redis here.
    const add = vi.fn().mockResolvedValue({ id: 'mock-job' });
    vi.doMock('@/lib/queue', () => ({ getQueue: () => ({ add }) }));
    const { requestGdprExport } = await import('@/lib/services/gdpr-export.service');

    const port = await makePort();
    const client = await makeClient({ portId: port.id });
    await db.insert(clientContacts).values({
      clientId: client.id,
      channel: 'email',
      value: 'p@example.com',
      isPrimary: true,
    });

    const { export: row } = await requestGdprExport({
      ...META(port.id),
      clientId: client.id,
      requestedBy: TEST_USER_ID,
      emailToClient: true,
    });
    expect(row.status).toBe('pending');
    expect(row.clientId).toBe(client.id);
    expect(add).toHaveBeenCalledWith(
      'gdpr-export',
      expect.objectContaining({ exportId: row.id, emailToClient: true }),
      // F3: BullMQ 5.x rejects colons in custom job IDs - switched to dash.
      expect.objectContaining({ jobId: `gdpr-export-${row.id}` }),
    );

    // Cleanup the mock so other tests don't see a stubbed queue.
    vi.doUnmock('@/lib/queue');

    const persisted = await db.query.gdprExports.findFirst({
      where: eq(gdprExports.id, row.id),
    });
    expect(persisted?.requestedBy).toBe(TEST_USER_ID);
  });

  it('refuses when emailToClient=true but no primary email exists and no override', async () => {
    vi.doMock('@/lib/queue', () => ({
      getQueue: () => ({ add: vi.fn().mockResolvedValue({ id: 'mock' }) }),
    }));
    const { requestGdprExport } = await import('@/lib/services/gdpr-export.service');

    const port = await makePort();
    const client = await makeClient({ portId: port.id });

    await expect(
      requestGdprExport({
        ...META(port.id),
        clientId: client.id,
        requestedBy: TEST_USER_ID,
        emailToClient: true,
      }),
    ).rejects.toThrow(ValidationError);

    vi.doUnmock('@/lib/queue');
  });
});
