import {
  EstimateExportFormat,
  EstimateStatus,
  EstimateVersionStatus,
  PermissionKey,
  SystemRole,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { describe, expect, it, vi } from "vitest";
import { estimateRouter } from "../router/estimate.js";
import { createCallerFactory } from "../trpc.js";

vi.mock("../sse/event-bus.js", () => ({
  emitAllocationCreated: vi.fn(),
  emitAllocationDeleted: vi.fn(),
  emitAllocationUpdated: vi.fn(),
}));

const createCaller = createCallerFactory(estimateRouter);

function createManagerCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "manager@example.com", name: "Manager", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_1",
      systemRole: SystemRole.MANAGER,
      permissionOverrides: null,
    },
  });
}

function createControllerCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "controller@example.com", name: "Controller", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_2",
      systemRole: SystemRole.CONTROLLER,
      permissionOverrides: null,
    },
  });
}

function createProtectedCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "viewer@example.com", name: "Viewer", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_3",
      systemRole: SystemRole.USER,
      permissionOverrides: null,
    },
  });
}

function createProtectedCallerWithOverrides(
  db: Record<string, unknown>,
  overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null,
) {
  return createCaller({
    session: {
      user: { email: "viewer@example.com", name: "Viewer", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_3",
      systemRole: SystemRole.USER,
      permissionOverrides: overrides,
    },
  });
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const baseVersion = {
  id: "ver_1",
  versionNumber: 1,
  label: null,
  status: EstimateVersionStatus.WORKING,
  lockedAt: null,
  notes: null,
  projectSnapshot: {},
  assumptions: [],
  scopeItems: [],
  demandLines: [],
  resourceSnapshots: [],
  metrics: [],
  exports: [],
  createdAt: new Date("2026-03-13"),
  updatedAt: new Date("2026-03-13"),
  commercialTerms: null,
};

const baseEstimate = {
  id: "est_1",
  name: "Test Estimate",
  projectId: null,
  opportunityId: null,
  baseCurrency: "EUR",
  status: EstimateStatus.DRAFT,
  latestVersionNumber: 1,
  createdAt: new Date("2026-03-13"),
  updatedAt: new Date("2026-03-13"),
  versions: [baseVersion],
  project: null,
};

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("estimate router", () => {
  // ─── list ──────────────────────────────────────────────────────────────────

  describe("list", () => {
    it("requires controller access", async () => {
      const caller = createProtectedCaller({});

      await expect(caller.list({})).rejects.toThrow(
        expect.objectContaining({
          code: "FORBIDDEN",
          message: "Controller access required",
        }),
      );
    });

    it("allows controllers to list estimates", async () => {
      const findMany = vi.fn().mockResolvedValue([baseEstimate]);
      const db = { estimate: { findMany } };

      const caller = createControllerCaller(db);
      const result = await caller.list({});

      expect(findMany).toHaveBeenCalled();
      expect(result).toHaveLength(1);
    });

    it("returns all estimates without filters", async () => {
      const findMany = vi.fn().mockResolvedValue([baseEstimate]);
      const db = { estimate: { findMany } };

      const caller = createManagerCaller(db);
      const result = await caller.list({});

      expect(findMany).toHaveBeenCalled();
      expect(result).toHaveLength(1);
    });

    it("passes projectId filter to query", async () => {
      const findMany = vi.fn().mockResolvedValue([]);
      const db = { estimate: { findMany } };

      const caller = createManagerCaller(db);
      await caller.list({ projectId: "project_1" });

      expect(findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ projectId: "project_1" }),
        }),
      );
    });

    it("passes status filter to query", async () => {
      const findMany = vi.fn().mockResolvedValue([]);
      const db = { estimate: { findMany } };

      const caller = createManagerCaller(db);
      await caller.list({ status: EstimateStatus.DRAFT });

      expect(findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ status: EstimateStatus.DRAFT }),
        }),
      );
    });

    it("passes query filter as OR clause", async () => {
      const findMany = vi.fn().mockResolvedValue([]);
      const db = { estimate: { findMany } };

      const caller = createManagerCaller(db);
      await caller.list({ query: "search" });

      expect(findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            OR: expect.any(Array),
          }),
        }),
      );
    });

    it("does not grant estimate listing through standalone viewCosts overrides", async () => {
      const caller = createProtectedCallerWithOverrides({}, {
        granted: [PermissionKey.VIEW_COSTS],
      });

      await expect(caller.list({})).rejects.toThrow(
        expect.objectContaining({
          code: "FORBIDDEN",
          message: "Controller access required",
        }),
      );
    });
  });

  // ─── getById ───────────────────────────────────────────────────────────────

  describe("getById", () => {
    it("returns the estimate when found", async () => {
      const findUnique = vi.fn().mockResolvedValue(baseEstimate);
      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      const result = await caller.getById({ id: "est_1" });

      expect(result.id).toBe("est_1");
      expect(result.name).toBe("Test Estimate");
    });

    it("throws NOT_FOUND when estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);
      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      await expect(caller.getById({ id: "missing" })).rejects.toThrow(
        expect.objectContaining({
          code: "NOT_FOUND",
          message: "Estimate not found",
        }),
      );
    });
  });

  describe("listVersions", () => {
    it("returns estimate versions ordered from newest to oldest", async () => {
      const findUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        name: "Test Estimate",
        status: EstimateStatus.DRAFT,
        latestVersionNumber: 2,
        versions: [
          {
            id: "ver_2",
            versionNumber: 2,
            label: "v2",
            status: EstimateVersionStatus.SUBMITTED,
            notes: null,
            lockedAt: new Date("2026-03-14"),
            createdAt: new Date("2026-03-14"),
            updatedAt: new Date("2026-03-14"),
            _count: {
              assumptions: 1,
              scopeItems: 2,
              demandLines: 3,
              resourceSnapshots: 4,
              exports: 5,
            },
          },
        ],
      });
      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      const result = await caller.listVersions({ estimateId: "est_1" });

      expect(result.versions).toHaveLength(1);
      expect(result.versions[0]?.id).toBe("ver_2");
      expect(findUnique).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "est_1" },
          select: expect.objectContaining({
            versions: expect.objectContaining({
              orderBy: { versionNumber: "desc" },
            }),
          }),
        }),
      );
    });

    it("throws NOT_FOUND when the estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);
      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      await expect(caller.listVersions({ estimateId: "missing" })).rejects.toThrow(
        expect.objectContaining({
          code: "NOT_FOUND",
          message: "Estimate not found",
        }),
      );
    });
  });

  describe("getVersionSnapshot", () => {
    it("returns aggregate counts and totals for the selected version", async () => {
      const findUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        name: "Test Estimate",
        status: EstimateStatus.DRAFT,
        baseCurrency: "EUR",
        versions: [
          {
            id: "ver_2",
            versionNumber: 2,
            label: "Revision 2",
            status: EstimateVersionStatus.SUBMITTED,
            notes: "Ready",
            lockedAt: new Date("2026-03-14"),
            createdAt: new Date("2026-03-14"),
            updatedAt: new Date("2026-03-15"),
            assumptions: [
              { id: "a_1", category: "delivery", key: "onsite", label: "Onsite" },
              { id: "a_2", category: "delivery", key: "travel", label: "Travel" },
              { id: "a_3", category: "commercial", key: "buffer", label: "Buffer" },
            ],
            scopeItems: [
              { id: "s_1", scopeType: "FEATURE", sequenceNo: 1, name: "Alpha" },
              { id: "s_2", scopeType: "FEATURE", sequenceNo: 2, name: "Beta" },
              { id: "s_3", scopeType: "SERVICE", sequenceNo: 3, name: "Gamma" },
            ],
            demandLines: [
              {
                id: "d_1",
                name: "Lead",
                chapter: "Delivery",
                hours: 10,
                costTotalCents: 100_00,
                priceTotalCents: 150_00,
                currency: "EUR",
              },
              {
                id: "d_2",
                name: "QA",
                chapter: null,
                hours: 5,
                costTotalCents: 50_00,
                priceTotalCents: 90_00,
                currency: "EUR",
              },
            ],
            resourceSnapshots: [
              {
                id: "r_1",
                displayName: "Alice",
                chapter: "Delivery",
                currency: "EUR",
                lcrCents: 10_000,
                ucrCents: 15_000,
              },
            ],
            exports: [
              {
                id: "x_1",
                format: EstimateExportFormat.XLSX,
                fileName: "estimate.xlsx",
                createdAt: new Date("2026-03-16"),
              },
            ],
          },
        ],
      });
      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      const result = await caller.getVersionSnapshot({ estimateId: "est_1" });

      expect(result.counts).toEqual({
        assumptions: 3,
        scopeItems: 3,
        demandLines: 2,
        resourceSnapshots: 1,
        exports: 1,
      });
      expect(result.totals).toMatchObject({
        hours: 15,
        costTotalCents: 15000,
        priceTotalCents: 24000,
      });
      expect(result.chapterBreakdown).toEqual([
        expect.objectContaining({ chapter: "Delivery", lineCount: 1, hours: 10 }),
        expect.objectContaining({ chapter: "Unassigned", lineCount: 1, hours: 5 }),
      ]);
      expect(result.scopeTypeBreakdown).toEqual([
        { scopeType: "FEATURE", count: 2 },
        { scopeType: "SERVICE", count: 1 },
      ]);
      expect(result.assumptionCategoryBreakdown).toEqual([
        { category: "delivery", count: 2 },
        { category: "commercial", count: 1 },
      ]);
    });

    it("throws NOT_FOUND when no matching version can be resolved", async () => {
      const findUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        name: "Test Estimate",
        status: EstimateStatus.DRAFT,
        baseCurrency: "EUR",
        versions: [],
      });
      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      await expect(
        caller.getVersionSnapshot({ estimateId: "est_1", versionId: "missing_version" }),
      ).rejects.toThrow(
        expect.objectContaining({
          code: "NOT_FOUND",
          message: "Estimate version not found",
        }),
      );
    });
  });

  // ─── create ────────────────────────────────────────────────────────────────

  describe("create", () => {
    it("creates an estimate with minimal valid input", async () => {
      const created = { ...baseEstimate, id: "est_new" };
      const estimateCreate = vi.fn().mockResolvedValue(created);
      const auditLogCreate = vi.fn().mockResolvedValue({});

      const db = {
        estimate: { create: estimateCreate },
        auditLog: { create: auditLogCreate },
      };

      const caller = createManagerCaller(db);
      const result = await caller.create({
        name: "New Estimate",
        baseCurrency: "EUR",
        status: EstimateStatus.DRAFT,
        assumptions: [],
        scopeItems: [],
        demandLines: [],
        resourceSnapshots: [],
        metrics: [],
      });

      expect(result.id).toBe("est_new");
      expect(estimateCreate).toHaveBeenCalled();
      expect(auditLogCreate).toHaveBeenCalled();
    });

    it("creates an estimate linked to a project", async () => {
      const created = { ...baseEstimate, id: "est_proj", projectId: "project_1" };
      const estimateCreate = vi.fn().mockResolvedValue(created);
      const projectFindUnique = vi.fn().mockResolvedValue({
        id: "project_1",
        shortCode: "PRJ1",
        name: "Test Project",
        status: "ACTIVE",
        startDate: new Date("2026-01-01"),
        endDate: new Date("2026-12-31"),
        orderType: "CHARGEABLE",
        allocationType: "INT",
        winProbability: 100,
        budgetCents: 100_000_00,
        responsiblePerson: "Test",
      });
      const auditLogCreate = vi.fn().mockResolvedValue({});

      const db = {
        estimate: { create: estimateCreate },
        project: { findUnique: projectFindUnique },
        auditLog: { create: auditLogCreate },
      };

      const caller = createManagerCaller(db);
      const result = await caller.create({
        projectId: "project_1",
        name: "Linked Estimate",
        baseCurrency: "EUR",
        status: EstimateStatus.DRAFT,
        assumptions: [],
        scopeItems: [],
        demandLines: [],
        resourceSnapshots: [],
        metrics: [],
      });

      expect(result.projectId).toBe("project_1");
      expect(projectFindUnique).toHaveBeenCalledWith(
        expect.objectContaining({ where: { id: "project_1" } }),
      );
    });

    it("throws NOT_FOUND when linked project does not exist", async () => {
      const projectFindUnique = vi.fn().mockResolvedValue(null);

      const db = {
        project: { findUnique: projectFindUnique },
        estimate: { create: vi.fn() },
        auditLog: { create: vi.fn() },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.create({
          projectId: "nonexistent",
          name: "Orphan Estimate",
          baseCurrency: "EUR",
          status: EstimateStatus.DRAFT,
          assumptions: [],
          scopeItems: [],
          demandLines: [],
          resourceSnapshots: [],
          metrics: [],
        }),
      ).rejects.toThrow(
        expect.objectContaining({
          code: "NOT_FOUND",
          message: "Project not found",
        }),
      );
    });
  });

  // ─── updateDraft ───────────────────────────────────────────────────────────

  describe("updateDraft", () => {
    it("updates a working draft successfully", async () => {
      const updated = { ...baseEstimate, name: "Updated Name" };
      const auditLogCreate = vi.fn().mockResolvedValue({});

      // The router delegates to @capakraken/application updateEstimateDraft.
      // The application function calls db.estimate.findUnique and then
      // db.estimateVersion.update (among others). We mock the DB calls
      // that the application layer uses under the hood.
      const findUnique = vi.fn().mockResolvedValue(baseEstimate);
      const updateVersion = vi.fn().mockResolvedValue({});
      const updateEstimate = vi.fn().mockResolvedValue({});
      const deleteAssumptions = vi.fn().mockResolvedValue({ count: 0 });
      const deleteScopeItems = vi.fn().mockResolvedValue({ count: 0 });
      const deleteDemandLines = vi.fn().mockResolvedValue({ count: 0 });
      const deleteSnapshots = vi.fn().mockResolvedValue({ count: 0 });
      const deleteMetrics = vi.fn().mockResolvedValue({ count: 0 });

      // After the transaction, the function re-fetches the estimate.
      const findUniqueRefreshed = vi.fn().mockResolvedValue(updated);

      const db = {
        estimate: {
          findUnique: vi
            .fn()
            // 1st call: resolve effectiveProjectId (rate card auto-fill)
            .mockResolvedValueOnce({ projectId: null })
            // 2nd call: application layer initial fetch
            .mockResolvedValueOnce(baseEstimate)
            // 3rd call: application layer post-update refetch
            .mockResolvedValueOnce(updated),
          update: updateEstimate,
        },
        estimateVersion: { update: updateVersion },
        estimateAssumption: {
          deleteMany: deleteAssumptions,
          createMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        scopeItem: {
          deleteMany: deleteScopeItems,
          create: vi.fn().mockResolvedValue({}),
        },
        estimateDemandLine: {
          deleteMany: deleteDemandLines,
          createMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        resourceCostSnapshot: {
          deleteMany: deleteSnapshots,
          createMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        estimateMetric: {
          deleteMany: deleteMetrics,
          createMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        auditLog: { create: auditLogCreate },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => {
          // Pass the same db as a transaction client
          return callback(db);
        }),
      };

      const caller = createManagerCaller(db);
      const result = await caller.updateDraft({
        id: "est_1",
        name: "Updated Name",
        assumptions: [],
        scopeItems: [],
        demandLines: [],
        resourceSnapshots: [],
        metrics: [],
      });

      expect(result.name).toBe("Updated Name");
      expect(auditLogCreate).toHaveBeenCalled();
    });

    it("throws NOT_FOUND when estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);

      const db = {
        estimate: { findUnique },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      // The application layer throws "Estimate not found" which the
      // router re-throws as a TRPCError NOT_FOUND.
      // However, since the application function is called directly (not mocked),
      // we need to mock the DB at the level the application function uses.
      const caller = createManagerCaller(db);
      await expect(
        caller.updateDraft({
          id: "missing",
          assumptions: [],
          scopeItems: [],
          demandLines: [],
          resourceSnapshots: [],
          metrics: [],
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("throws PRECONDITION_FAILED when estimate has no working version", async () => {
      const estimateNoWorking = {
        ...baseEstimate,
        versions: [
          { ...baseVersion, status: EstimateVersionStatus.SUBMITTED },
        ],
      };
      const findUnique = vi.fn().mockResolvedValue(estimateNoWorking);

      const db = {
        estimate: { findUnique },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.updateDraft({
          id: "est_1",
          assumptions: [],
          scopeItems: [],
          demandLines: [],
          resourceSnapshots: [],
          metrics: [],
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "PRECONDITION_FAILED" }),
      );
    });
  });

  // ─── submitVersion ─────────────────────────────────────────────────────────

  describe("submitVersion", () => {
    it("submits a working version and transitions to IN_REVIEW", async () => {
      const existing = {
        ...baseEstimate,
        status: EstimateStatus.DRAFT,
        versions: [
          { ...baseVersion, status: EstimateVersionStatus.WORKING },
        ],
      };
      const afterSubmit = {
        ...existing,
        status: EstimateStatus.IN_REVIEW,
        versions: [
          {
            ...baseVersion,
            status: EstimateVersionStatus.SUBMITTED,
            lockedAt: new Date("2026-03-13"),
          },
        ],
      };

      const findUnique = vi
        .fn()
        .mockResolvedValueOnce(existing)
        .mockResolvedValueOnce(afterSubmit);
      const updateMany = vi.fn().mockResolvedValue({ count: 0 });
      const updateVersion = vi.fn().mockResolvedValue({});
      const updateEstimate = vi.fn().mockResolvedValue({});
      const auditLogCreate = vi.fn().mockResolvedValue({});

      const db = {
        estimate: {
          findUnique,
          update: updateEstimate,
        },
        estimateVersion: {
          updateMany,
          update: updateVersion,
        },
        auditLog: { create: auditLogCreate },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) =>
          callback({
            estimateVersion: { updateMany, update: updateVersion },
            estimate: { update: updateEstimate },
          }),
        ),
      };

      const caller = createManagerCaller(db);
      const result = await caller.submitVersion({ estimateId: "est_1" });

      expect(result.status).toBe(EstimateStatus.IN_REVIEW);
      expect(auditLogCreate).toHaveBeenCalled();
    });

    it("throws NOT_FOUND when estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);

      const db = {
        estimate: { findUnique },
        estimateVersion: { updateMany: vi.fn(), update: vi.fn() },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.submitVersion({ estimateId: "missing" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("throws PRECONDITION_FAILED when no working version exists", async () => {
      const noWorking = {
        ...baseEstimate,
        versions: [
          { ...baseVersion, status: EstimateVersionStatus.SUBMITTED },
        ],
      };
      const findUnique = vi.fn().mockResolvedValue(noWorking);

      const db = {
        estimate: { findUnique },
        estimateVersion: { updateMany: vi.fn(), update: vi.fn() },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.submitVersion({ estimateId: "est_1" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "PRECONDITION_FAILED" }),
      );
    });
  });

  // ─── approveVersion ────────────────────────────────────────────────────────

  describe("approveVersion", () => {
    it("approves a submitted version", async () => {
      const existing = {
        ...baseEstimate,
        status: EstimateStatus.IN_REVIEW,
        versions: [
          {
            ...baseVersion,
            status: EstimateVersionStatus.SUBMITTED,
            lockedAt: new Date("2026-03-13"),
          },
        ],
      };
      const afterApprove = {
        ...existing,
        status: EstimateStatus.APPROVED,
        versions: [
          {
            ...baseVersion,
            status: EstimateVersionStatus.APPROVED,
            lockedAt: new Date("2026-03-13"),
          },
        ],
      };

      const findUnique = vi
        .fn()
        .mockResolvedValueOnce(existing)
        .mockResolvedValueOnce(afterApprove);
      const updateMany = vi.fn().mockResolvedValue({ count: 0 });
      const updateVersion = vi.fn().mockResolvedValue({});
      const updateEstimate = vi.fn().mockResolvedValue({});
      const auditLogCreate = vi.fn().mockResolvedValue({});

      const db = {
        estimate: {
          findUnique,
          update: updateEstimate,
        },
        estimateVersion: {
          updateMany,
          update: updateVersion,
        },
        auditLog: { create: auditLogCreate },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) =>
          callback({
            estimateVersion: { updateMany, update: updateVersion },
            estimate: { update: updateEstimate },
          }),
        ),
      };

      const caller = createManagerCaller(db);
      const result = await caller.approveVersion({ estimateId: "est_1" });

      expect(result.status).toBe(EstimateStatus.APPROVED);
      expect(auditLogCreate).toHaveBeenCalled();
    });

    it("throws PRECONDITION_FAILED when no submitted version exists", async () => {
      const noSubmitted = {
        ...baseEstimate,
        status: EstimateStatus.DRAFT,
        versions: [
          { ...baseVersion, status: EstimateVersionStatus.WORKING },
        ],
      };
      const findUnique = vi.fn().mockResolvedValue(noSubmitted);

      const db = {
        estimate: { findUnique },
        estimateVersion: { updateMany: vi.fn(), update: vi.fn() },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.approveVersion({ estimateId: "est_1" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "PRECONDITION_FAILED" }),
      );
    });
  });

  // ─── createRevision ────────────────────────────────────────────────────────

  describe("createRevision", () => {
    it("creates a new working version from the latest locked version", async () => {
      const approved = {
        ...baseEstimate,
        status: EstimateStatus.APPROVED,
        latestVersionNumber: 1,
        versions: [
          {
            ...baseVersion,
            status: EstimateVersionStatus.APPROVED,
            lockedAt: new Date("2026-03-13"),
          },
        ],
      };
      const afterRevision = {
        ...approved,
        status: EstimateStatus.DRAFT,
        latestVersionNumber: 2,
        versions: [
          {
            ...baseVersion,
            status: EstimateVersionStatus.APPROVED,
            lockedAt: new Date("2026-03-13"),
          },
          {
            ...baseVersion,
            id: "ver_2",
            versionNumber: 2,
            status: EstimateVersionStatus.WORKING,
            lockedAt: null,
          },
        ],
      };

      const findUnique = vi
        .fn()
        .mockResolvedValueOnce(approved)
        .mockResolvedValueOnce(afterRevision);
      const createVersion = vi.fn().mockResolvedValue({ id: "ver_2" });
      const createAssumptions = vi.fn().mockResolvedValue({ count: 0 });
      const createScopeItem = vi.fn().mockResolvedValue({ id: "scope_new" });
      const createDemandLines = vi.fn().mockResolvedValue({ count: 0 });
      const createSnapshots = vi.fn().mockResolvedValue({ count: 0 });
      const createMetrics = vi.fn().mockResolvedValue({ count: 0 });
      const updateEstimate = vi.fn().mockResolvedValue({});
      const auditLogCreate = vi.fn().mockResolvedValue({});

      const db = {
        estimate: {
          findUnique,
          update: updateEstimate,
        },
        estimateVersion: { create: createVersion },
        estimateAssumption: { createMany: createAssumptions },
        scopeItem: { create: createScopeItem },
        estimateDemandLine: { createMany: createDemandLines },
        resourceCostSnapshot: { createMany: createSnapshots },
        estimateMetric: { createMany: createMetrics },
        auditLog: { create: auditLogCreate },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) =>
          callback({
            estimateVersion: { create: createVersion },
            estimateAssumption: { createMany: createAssumptions },
            scopeItem: { create: createScopeItem },
            estimateDemandLine: { createMany: createDemandLines },
            resourceCostSnapshot: { createMany: createSnapshots },
            estimateMetric: { createMany: createMetrics },
            estimate: { update: updateEstimate },
          }),
        ),
      };

      const caller = createManagerCaller(db);
      const result = await caller.createRevision({ estimateId: "est_1" });

      expect(result.latestVersionNumber).toBe(2);
      expect(auditLogCreate).toHaveBeenCalled();
    });

    it("throws NOT_FOUND when estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);

      const db = {
        estimate: { findUnique },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.createRevision({ estimateId: "missing" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("throws PRECONDITION_FAILED when estimate already has a working version", async () => {
      const withWorking = {
        ...baseEstimate,
        versions: [
          { ...baseVersion, status: EstimateVersionStatus.WORKING },
        ],
      };
      const findUnique = vi.fn().mockResolvedValue(withWorking);

      const db = {
        estimate: { findUnique },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.createRevision({ estimateId: "est_1" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "PRECONDITION_FAILED" }),
      );
    });
  });

  // ─── clone ─────────────────────────────────────────────────────────────────

  describe("clone", () => {
    it("clones an estimate successfully", async () => {
      const source = {
        ...baseEstimate,
        id: "est_source",
        name: "Original",
        versions: [
          {
            ...baseVersion,
            assumptions: [],
            scopeItems: [],
            demandLines: [],
            resourceSnapshots: [],
            metrics: [],
          },
        ],
      };
      const cloned = {
        ...baseEstimate,
        id: "est_clone",
        name: "Copy of Original",
      };

      const estimateFindUnique = vi
        .fn()
        .mockResolvedValueOnce(source) // cloneEstimate reads source
        .mockResolvedValueOnce(cloned); // cloneEstimate re-fetches
      const estimateCreate = vi.fn().mockResolvedValue(cloned);
      const auditLogCreate = vi.fn().mockResolvedValue({});

      const db = {
        estimate: {
          findUnique: estimateFindUnique,
          create: estimateCreate,
        },
        auditLog: { create: auditLogCreate },
      };

      const caller = createManagerCaller(db);
      const result = await caller.clone({ sourceEstimateId: "est_source" });

      expect(result.id).toBe("est_clone");
      expect(auditLogCreate).toHaveBeenCalled();
    });

    it("throws NOT_FOUND when source estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);

      const db = {
        estimate: { findUnique },
        auditLog: { create: vi.fn() },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.clone({ sourceEstimateId: "missing" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });
  });

  // ─── getCommercialTerms ────────────────────────────────────────────────────

  describe("getCommercialTerms", () => {
    it("returns defaults when commercialTerms is null", async () => {
      const findUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        versions: [{ id: "ver_1", commercialTerms: null }],
      });

      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      const result = await caller.getCommercialTerms({ estimateId: "est_1" });

      expect(result.versionId).toBe("ver_1");
      expect(result.terms.pricingModel).toBe("fixed_price");
      expect(result.terms.contingencyPercent).toBe(0);
      expect(result.terms.discountPercent).toBe(0);
      expect(result.terms.paymentTermDays).toBe(30);
      expect(result.terms.paymentMilestones).toEqual([]);
      expect(result.terms.warrantyMonths).toBe(0);
    });

    it("returns saved terms when commercialTerms is set", async () => {
      const savedTerms = {
        pricingModel: "time_and_materials",
        contingencyPercent: 10,
        discountPercent: 5,
        paymentTermDays: 60,
        paymentMilestones: [],
        warrantyMonths: 6,
      };

      const findUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        versions: [{ id: "ver_1", commercialTerms: savedTerms }],
      });

      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      const result = await caller.getCommercialTerms({ estimateId: "est_1" });

      expect(result.terms.pricingModel).toBe("time_and_materials");
      expect(result.terms.contingencyPercent).toBe(10);
      expect(result.terms.discountPercent).toBe(5);
      expect(result.terms.paymentTermDays).toBe(60);
      expect(result.terms.warrantyMonths).toBe(6);
    });

    it("throws NOT_FOUND when estimate has no versions", async () => {
      const findUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        versions: [],
      });

      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      await expect(
        caller.getCommercialTerms({ estimateId: "est_1" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("throws NOT_FOUND when estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);
      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      await expect(
        caller.getCommercialTerms({ estimateId: "missing" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });
  });

  // ─── updateCommercialTerms ─────────────────────────────────────────────────

  describe("updateCommercialTerms", () => {
    it("saves commercial terms on a working version", async () => {
      const estimateFindUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        versions: [{ id: "ver_1", status: "WORKING" }],
      });
      const updateVersion = vi.fn().mockResolvedValue({});
      const auditLogCreate = vi.fn().mockResolvedValue({});

      const db = {
        estimate: { findUnique: estimateFindUnique },
        estimateVersion: { update: updateVersion },
        auditLog: { create: auditLogCreate },
      };

      const caller = createManagerCaller(db);
      const result = await caller.updateCommercialTerms({
        estimateId: "est_1",
        terms: {
          pricingModel: "time_and_materials",
          contingencyPercent: 15,
          discountPercent: 0,
          paymentTermDays: 45,
          paymentMilestones: [],
          warrantyMonths: 3,
        },
      });

      expect(result.versionId).toBe("ver_1");
      expect(result.terms.pricingModel).toBe("time_and_materials");
      expect(result.terms.contingencyPercent).toBe(15);
      expect(updateVersion).toHaveBeenCalledWith(
        expect.objectContaining({ where: { id: "ver_1" } }),
      );
      expect(auditLogCreate).toHaveBeenCalled();
    });

    it("throws PRECONDITION_FAILED on a non-working version", async () => {
      const estimateFindUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        versions: [{ id: "ver_1", status: "SUBMITTED" }],
      });

      const db = {
        estimate: { findUnique: estimateFindUnique },
        estimateVersion: { update: vi.fn() },
        auditLog: { create: vi.fn() },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.updateCommercialTerms({
          estimateId: "est_1",
          terms: {
            pricingModel: "fixed_price",
            contingencyPercent: 0,
            discountPercent: 0,
            paymentTermDays: 30,
            paymentMilestones: [],
            warrantyMonths: 0,
          },
        }),
      ).rejects.toThrow(
        expect.objectContaining({
          code: "PRECONDITION_FAILED",
          message: "Commercial terms can only be edited on working versions",
        }),
      );
    });

    it("throws NOT_FOUND when estimate version is missing", async () => {
      const estimateFindUnique = vi.fn().mockResolvedValue({
        id: "est_1",
        versions: [],
      });

      const db = {
        estimate: { findUnique: estimateFindUnique },
        estimateVersion: { update: vi.fn() },
        auditLog: { create: vi.fn() },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.updateCommercialTerms({
          estimateId: "est_1",
          terms: {
            pricingModel: "fixed_price",
            contingencyPercent: 0,
            discountPercent: 0,
            paymentTermDays: 30,
            paymentMilestones: [],
            warrantyMonths: 0,
          },
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });
  });

  // ─── createExport ──────────────────────────────────────────────────────────

  describe("createExport", () => {
    it("generates an export artifact for a version", async () => {
      const createdAt = new Date("2026-03-13T08:00:00.000Z");
      const estimateWithExport = {
        id: "est_1",
        name: "CGI Estimate",
        baseCurrency: "EUR",
        createdAt,
        updatedAt: createdAt,
        status: EstimateStatus.APPROVED,
        latestVersionNumber: 1,
        project: null,
        versions: [
          {
            id: "ver_1",
            versionNumber: 1,
            status: EstimateVersionStatus.APPROVED,
            lockedAt: new Date("2026-03-13"),
            createdAt,
            updatedAt: createdAt,
            assumptions: [],
            scopeItems: [],
            demandLines: [],
            resourceSnapshots: [],
            metrics: [],
            exports: [
              {
                id: "exp_1",
                format: EstimateExportFormat.JSON,
                fileName: "cgi-estimate-v1.json",
                payload: {},
              },
            ],
            projectSnapshot: {},
          },
        ],
      };

      // createEstimateExport first reads the estimate, creates an export,
      // then re-reads. We mock both calls.
      const estimateFindUnique = vi
        .fn()
        .mockResolvedValueOnce({
          ...estimateWithExport,
          versions: [
            {
              ...estimateWithExport.versions[0],
              exports: [],
            },
          ],
        })
        .mockResolvedValueOnce(estimateWithExport);
      const createExport = vi.fn().mockResolvedValue({ id: "exp_1" });
      const auditLogCreate = vi.fn().mockResolvedValue({});

      const db = {
        estimate: { findUnique: estimateFindUnique },
        estimateExport: { create: createExport },
        auditLog: { create: auditLogCreate },
      };

      const caller = createManagerCaller(db);
      const result = await caller.createExport({
        estimateId: "est_1",
        format: EstimateExportFormat.JSON,
      });

      expect(result.id).toBe("est_1");
      expect(result.versions[0]?.exports).toHaveLength(1);
      expect(createExport).toHaveBeenCalled();
      expect(auditLogCreate).toHaveBeenCalled();
    });

    it("throws NOT_FOUND when estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);

      const db = {
        estimate: { findUnique },
        estimateExport: { create: vi.fn() },
        auditLog: { create: vi.fn() },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.createExport({
          estimateId: "missing",
          format: EstimateExportFormat.JSON,
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });
  });

  // ─── createPlanningHandoff ─────────────────────────────────────────────────

  describe("createPlanningHandoff", () => {
    it("throws NOT_FOUND when estimate does not exist", async () => {
      const findUnique = vi.fn().mockResolvedValue(null);

      const db = {
        estimate: { findUnique },
        project: { findUnique: vi.fn() },
        demandRequirement: { findMany: vi.fn() },
        assignment: { findMany: vi.fn() },
        resource: { findMany: vi.fn() },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.createPlanningHandoff({ estimateId: "missing" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("throws PRECONDITION_FAILED when estimate has no approved version", async () => {
      const draftOnly = {
        ...baseEstimate,
        status: EstimateStatus.DRAFT,
        projectId: "project_1",
        versions: [
          { ...baseVersion, status: EstimateVersionStatus.WORKING },
        ],
      };
      const findUnique = vi.fn().mockResolvedValue(draftOnly);

      const db = {
        estimate: { findUnique },
        project: { findUnique: vi.fn() },
        demandRequirement: { findMany: vi.fn() },
        assignment: { findMany: vi.fn() },
        resource: { findMany: vi.fn() },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.createPlanningHandoff({ estimateId: "est_1" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "PRECONDITION_FAILED" }),
      );
    });

    it("throws PRECONDITION_FAILED when estimate is not linked to a project", async () => {
      const noProject = {
        ...baseEstimate,
        projectId: null,
        status: EstimateStatus.APPROVED,
        versions: [
          {
            ...baseVersion,
            status: EstimateVersionStatus.APPROVED,
            lockedAt: new Date("2026-03-13"),
          },
        ],
      };
      const findUnique = vi.fn().mockResolvedValue(noProject);

      const db = {
        estimate: { findUnique },
        project: { findUnique: vi.fn() },
        demandRequirement: { findMany: vi.fn().mockResolvedValue([]) },
        assignment: { findMany: vi.fn().mockResolvedValue([]) },
        resource: { findMany: vi.fn().mockResolvedValue([]) },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.createPlanningHandoff({ estimateId: "est_1" }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "PRECONDITION_FAILED" }),
      );
    });

    it("throws PRECONDITION_FAILED for demand-line project windows without working days", async () => {
      const approvedEstimate = {
        ...baseEstimate,
        projectId: "project_1",
        status: EstimateStatus.APPROVED,
        versions: [
          {
            ...baseVersion,
            id: "ver_approved",
            status: EstimateVersionStatus.APPROVED,
            lockedAt: new Date("2026-03-13"),
            demandLines: [
              {
                id: "line_1",
                name: "Staffing Gap",
                hours: 16,
                fte: 1,
                resourceId: null,
              },
            ],
          },
        ],
      };
      const findUnique = vi.fn().mockResolvedValue(approvedEstimate);
      const projectFindUnique = vi.fn().mockResolvedValue({
        id: "project_1",
        shortCode: "PRJ1",
        name: "Weekend Project",
        status: "ACTIVE",
        startDate: new Date("2026-03-15"),
        endDate: new Date("2026-03-15"),
        orderType: "CHARGEABLE",
        allocationType: "INT",
        winProbability: 100,
        budgetCents: 100_000_00,
        responsiblePerson: "Test",
      });

      const db = {
        estimate: { findUnique },
        project: { findUnique: projectFindUnique },
        demandRequirement: { findMany: vi.fn().mockResolvedValue([]) },
        assignment: { findMany: vi.fn().mockResolvedValue([]) },
        resource: { findMany: vi.fn().mockResolvedValue([]) },
        auditLog: { create: vi.fn() },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.createPlanningHandoff({ estimateId: "est_1" }),
      ).rejects.toThrow(
        expect.objectContaining({
          code: "PRECONDITION_FAILED",
          message: 'Project window has no working days for demand line "Staffing Gap"',
        }),
      );
    });
  });

  // ─── RBAC ──────────────────────────────────────────────────────────────────

  describe("RBAC enforcement", () => {
    it("blocks USER role from controller-only getById", async () => {
      const db = { estimate: { findUnique: vi.fn() } };
      const caller = createProtectedCaller(db);

      await expect(caller.getById({ id: "est_1" })).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });

    it("blocks USER role from manager-only create", async () => {
      const db = {
        estimate: { create: vi.fn() },
        auditLog: { create: vi.fn() },
      };
      const caller = createProtectedCaller(db);

      await expect(
        caller.create({
          name: "Test",
          baseCurrency: "EUR",
          assumptions: [],
          scopeItems: [],
          demandLines: [],
          resourceSnapshots: [],
          metrics: [],
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });

    it("allows CONTROLLER to access getById", async () => {
      const findUnique = vi.fn().mockResolvedValue(baseEstimate);
      const db = { estimate: { findUnique } };

      const caller = createControllerCaller(db);
      const result = await caller.getById({ id: "est_1" });
      expect(result.id).toBe("est_1");
    });
  });
});
