/**
 * Task 8 - aggregated projection (TDD).
 *
 * Tests for:
 *  1. listFilesAggregatedByEntity (4 cases)
 *  2. listInflightWorkflowsAggregatedByEntity (1 case)
 *  3. applyEntityFkFromFolder (3 cases)
 *
 * Fixture convention: makePort / makeClient / makeCompany / makeYacht from
 * helpers/factories; TEST_USER_ID resolved once via beforeAll from a seeded
 * user - same pattern as document-folders-system-folders.test.ts.
 */

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

import { db } from '@/lib/db';
import { files, documents, documentFolders } from '@/lib/db/schema/documents';
import { clients } from '@/lib/db/schema/clients';
import { user } from '@/lib/db/schema/users';
import { yachts } from '@/lib/db/schema/yachts';
import { ensureSystemRoots, ensureEntityFolder } from '@/lib/services/document-folders.service';
import { listFilesAggregatedByEntity, applyEntityFkFromFolder } from '@/lib/services/files';
import { listInflightWorkflowsAggregatedByEntity } from '@/lib/services/documents.service';
import { makePort, makeClient, makeCompany, makeYacht, makeMembership } from '../helpers/factories';

let TEST_USER_ID = '';

beforeAll(async () => {
  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;
});

// ─── Helper to insert a file row directly ────────────────────────────────────

async function insertFile(
  portId: string,
  overrides: {
    clientId?: string | null;
    companyId?: string | null;
    yachtId?: string | null;
    folderId?: string | null;
  } = {},
) {
  const [row] = await db
    .insert(files)
    .values({
      portId,
      clientId: overrides.clientId ?? null,
      companyId: overrides.companyId ?? null,
      yachtId: overrides.yachtId ?? null,
      folderId: overrides.folderId ?? null,
      filename: `file-${crypto.randomUUID().slice(0, 8)}.pdf`,
      originalName: `original-${crypto.randomUUID().slice(0, 8)}.pdf`,
      mimeType: 'application/pdf',
      sizeBytes: '1024',
      storagePath: `test/${crypto.randomUUID()}.pdf`,
      storageBucket: 'crm-files',
      uploadedBy: TEST_USER_ID,
    })
    .returning();
  return row!;
}

// ─── listFilesAggregatedByEntity ─────────────────────────────────────────────

describe('files service · listFilesAggregatedByEntity', () => {
  describe('groups DIRECTLY ATTACHED + FROM COMPANY + FROM YACHT for a client view', () => {
    let portId: string;
    let clientId: string;
    let companyId: string;
    let yachtId: string;

    beforeEach(async () => {
      const port = await makePort();
      portId = port.id;

      const client = await makeClient({ portId });
      clientId = client.id;

      const company = await makeCompany({ portId });
      companyId = company.id;

      await makeMembership({ companyId, clientId });

      const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: clientId });
      yachtId = yacht.id;

      // Three files: directly on client, on company, on yacht
      await insertFile(portId, { clientId });
      await insertFile(portId, { companyId });
      await insertFile(portId, { yachtId });
    });

    it('returns DIRECTLY ATTACHED, FROM COMPANY, and FROM YACHT groups', async () => {
      const result = await listFilesAggregatedByEntity(portId, 'client', clientId);
      const labels = result.groups.map((g) => g.label);

      expect(labels).toContain('DIRECTLY ATTACHED');
      expect(labels.some((l) => l.startsWith('FROM COMPANY'))).toBe(true);
      expect(labels.some((l) => l.startsWith('FROM YACHT'))).toBe(true);
    });

    it('each group has the correct source tag', async () => {
      const result = await listFilesAggregatedByEntity(portId, 'client', clientId);
      const sourceMap: Record<string, string> = {};
      for (const g of result.groups) {
        sourceMap[g.label] = g.source;
      }
      expect(sourceMap['DIRECTLY ATTACHED']).toBe('direct');
      const companyGroup = result.groups.find((g) => g.label.startsWith('FROM COMPANY'));
      expect(companyGroup?.source).toBe('company');
      const yachtGroup = result.groups.find((g) => g.label.startsWith('FROM YACHT'));
      expect(yachtGroup?.source).toBe('yacht');
    });

    it('file rows carry signedFromDocumentId=null when no workflow references them', async () => {
      const result = await listFilesAggregatedByEntity(portId, 'client', clientId);
      const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED');
      expect(directGroup).toBeDefined();
      for (const f of directGroup!.files) {
        expect(f).toHaveProperty('signedFromDocumentId');
        expect(f.signedFromDocumentId).toBeNull();
      }
    });
  });

  describe('caps each group at 20 rows + surfaces total', () => {
    let portId: string;
    let clientId: string;

    beforeEach(async () => {
      const port = await makePort();
      portId = port.id;

      const client = await makeClient({ portId });
      clientId = client.id;

      // Insert 25 files all with clientId
      await Promise.all(Array.from({ length: 25 }, () => insertFile(portId, { clientId })));
    });

    it('DIRECTLY ATTACHED group has 20 files and total=25', async () => {
      const result = await listFilesAggregatedByEntity(portId, 'client', clientId);
      const group = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED');
      expect(group).toBeDefined();
      expect(group!.files).toHaveLength(20);
      expect(group!.total).toBe(25);
    });
  });

  describe('file-FK snapshot survives yacht transfer', () => {
    let portId: string;
    let johnId: string;
    let maryId: string;
    let yachtId: string;

    beforeEach(async () => {
      const port = await makePort();
      portId = port.id;

      const john = await makeClient({ portId, overrides: { fullName: 'John Smith' } });
      johnId = john.id;

      const mary = await makeClient({ portId, overrides: { fullName: 'Mary Jones' } });
      maryId = mary.id;

      const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: johnId });
      yachtId = yacht.id;

      // File attached to the yacht at the time john owns it
      await insertFile(portId, { yachtId, clientId: johnId });

      // Transfer yacht to Mary (update currentOwner in place - simulates transfer)
      await db
        .update(yachts)
        .set({ currentOwnerType: 'client', currentOwnerId: maryId })
        .where(and(eq(yachts.id, yachtId), eq(yachts.portId, portId)));
    });

    it("John's view still shows the file via yachtId FK", async () => {
      const result = await listFilesAggregatedByEntity(portId, 'client', johnId);
      // clientId=johnId → DIRECTLY ATTACHED group
      const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED');
      expect(directGroup).toBeDefined();
      expect(directGroup!.files.length).toBeGreaterThan(0);
    });

    it("Mary's view does NOT see john's file (it has clientId=john, not mary)", async () => {
      const result = await listFilesAggregatedByEntity(portId, 'client', maryId);
      // Mary owns the yacht now, so FROM YACHT group will appear - but the
      // file has clientId=johnId (snapshotted FK), so it WON'T appear under
      // Mary's DIRECTLY ATTACHED. The FROM YACHT group WILL appear since the
      // file still has yachtId set.
      // The key invariant: there is no DIRECTLY ATTACHED for Mary.
      const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED');
      expect(directGroup).toBeUndefined();
    });
  });

  describe('cross-port leakage rejected', () => {
    let portId: string;
    let otherPortId: string;
    let otherClientId: string;

    beforeEach(async () => {
      const port = await makePort();
      portId = port.id;

      const otherPort = await makePort();
      otherPortId = otherPort.id;

      const otherClient = await makeClient({ portId: otherPortId });
      otherClientId = otherClient.id;

      // File for other client in other port
      await insertFile(otherPortId, { clientId: otherClientId });
    });

    it('returns empty groups when entity belongs to a different port', async () => {
      const result = await listFilesAggregatedByEntity(portId, 'client', otherClientId);
      expect(result.groups).toHaveLength(0);
    });
  });

  describe('signedFromDocumentId reverse-link', () => {
    let portId: string;
    let clientId: string;

    beforeEach(async () => {
      const port = await makePort();
      portId = port.id;

      const client = await makeClient({ portId });
      clientId = client.id;
    });

    it('surfaces signedFromDocumentId when a workflow references the file', async () => {
      const fileRow = await insertFile(portId, { clientId });

      // Insert a document row with signedFileId pointing at the file
      const [doc] = await db
        .insert(documents)
        .values({
          portId,
          clientId,
          documentType: 'contract',
          title: 'Signed Contract',
          status: 'completed',
          signedFileId: fileRow.id,
          createdBy: TEST_USER_ID,
        })
        .returning();

      const result = await listFilesAggregatedByEntity(portId, 'client', clientId);
      const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED');
      expect(directGroup).toBeDefined();

      const linkedFile = directGroup!.files.find((f) => f.id === fileRow.id);
      expect(linkedFile).toBeDefined();
      expect(linkedFile!.signedFromDocumentId).toBe(doc!.id);
    });
  });
});

// ─── listInflightWorkflowsAggregatedByEntity ─────────────────────────────────

describe('documents service · listInflightWorkflowsAggregatedByEntity', () => {
  let portId: string;
  let clientId: string;

  beforeEach(async () => {
    const port = await makePort();
    portId = port.id;

    const client = await makeClient({ portId });
    clientId = client.id;
  });

  it('returns in-flight workflows in DIRECTLY ATTACHED group, hides completed', async () => {
    // Insert two documents: one in-flight (status='sent'), one completed
    await db.insert(documents).values([
      {
        portId,
        clientId,
        documentType: 'contract',
        title: 'In-flight Doc',
        status: 'sent',
        createdBy: TEST_USER_ID,
      },
      {
        portId,
        clientId,
        documentType: 'contract',
        title: 'Completed Doc',
        status: 'completed',
        createdBy: TEST_USER_ID,
      },
    ]);

    const result = await listInflightWorkflowsAggregatedByEntity(portId, 'client', clientId);

    const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED');
    expect(directGroup).toBeDefined();
    expect(directGroup!.workflows).toHaveLength(1);
    expect(directGroup!.workflows[0]!.status).toBe('sent');
    expect(directGroup!.workflows[0]!.title).toBe('In-flight Doc');
  });

  it('rejects cross-port leakage with defense-in-depth port filter', async () => {
    const otherPort = await makePort();
    const [otherClient] = await db
      .insert(clients)
      .values({ portId: otherPort.id, fullName: 'Other Port Client' })
      .returning();
    const result = await listInflightWorkflowsAggregatedByEntity(portId, 'client', otherClient!.id);
    expect(result.groups).toEqual([]);
  });
});

// ─── applyEntityFkFromFolder ──────────────────────────────────────────────────

describe('files service · applyEntityFkFromFolder', () => {
  let portId: string;
  let clientId: string;
  let entityFolderId: string;

  beforeEach(async () => {
    const port = await makePort();
    portId = port.id;

    await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
    await ensureSystemRoots(portId, TEST_USER_ID);

    const client = await makeClient({ portId });
    clientId = client.id;

    const folder = await ensureEntityFolder(portId, 'client', clientId, TEST_USER_ID);
    entityFolderId = folder.id;
  });

  it('sets clientId when uploading into a client entity folder', async () => {
    const out = await applyEntityFkFromFolder(portId, { folderId: entityFolderId, clientId: null });
    expect(out.clientId).toBe(clientId);
  });

  it('preserves existing entity FK when already set', async () => {
    const out = await applyEntityFkFromFolder(portId, {
      folderId: entityFolderId,
      clientId: 'pre-existing-id',
    });
    expect(out.clientId).toBe('pre-existing-id');
  });

  it('is a no-op for non-system folders', async () => {
    const [userFolder] = await db
      .insert(documentFolders)
      .values({
        portId,
        parentId: null,
        name: 'My templates',
        createdBy: TEST_USER_ID,
      })
      .returning();
    const out = await applyEntityFkFromFolder(portId, {
      folderId: userFolder!.id,
      clientId: null,
    });
    expect(out.clientId).toBeNull();
  });
});
