import { OrderType, AllocationType, PermissionKey, ProjectStatus, SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { invalidateDashboardCache } from "../lib/cache.js";
import { logger } from "../lib/logger.js";
import { projectRouter } from "../router/project.js";
import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
import { createCallerFactory } from "../trpc.js";

vi.mock("@capakraken/application", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@capakraken/application")>();
  return {
    ...actual,
    countPlanningEntries: vi.fn().mockResolvedValue({ countsByProjectId: new Map() }),
    listAssignmentBookings: vi.fn().mockResolvedValue([]),
  };
});

vi.mock("../router/blueprint-validation.js", () => ({
  assertBlueprintDynamicFields: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../router/project-planning-read-model.js", () => ({
  loadProjectPlanningReadModel: vi.fn().mockResolvedValue({
    readModel: { assignments: [], demands: [] },
  }),
}));

vi.mock("../lib/cache.js", () => ({
  invalidateDashboardCache: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../lib/webhook-dispatcher.js", () => ({
  dispatchWebhooks: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../lib/logger.js", () => ({
  logger: {
    error: vi.fn(),
    warn: vi.fn(),
    info: vi.fn(),
    debug: vi.fn(),
  },
}));

vi.mock("../ai-client.js", () => ({
  isDalleConfigured: vi.fn().mockReturnValue(false),
  createDalleClient: vi.fn(),
  parseAiError: vi.fn(),
}));

const createCaller = createCallerFactory(projectRouter);

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: "mgr_1",
      systemRole: SystemRole.MANAGER,
      permissionOverrides: null,
    },
  });
}

function createAdminCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "admin@example.com", name: "Admin", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "admin_1",
      systemRole: SystemRole.ADMIN,
      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: "ctrl_1",
      systemRole: SystemRole.CONTROLLER,
      permissionOverrides: null,
    },
  });
}

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

function createUnauthenticatedCaller(db: Record<string, unknown>) {
  return createCaller({
    session: null,
    db: db as never,
    dbUser: null,
  });
}

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

const sampleProject = {
  id: "project_1",
  shortCode: "PRJ-001",
  name: "Test Project",
  orderType: OrderType.CHARGEABLE,
  allocationType: AllocationType.INT,
  winProbability: 80,
  budgetCents: 500_000_00,
  startDate: new Date("2026-01-01"),
  endDate: new Date("2026-06-30"),
  status: ProjectStatus.ACTIVE,
  responsiblePerson: "Alice",
  dynamicFields: {},
  staffingReqs: [],
  blueprintId: null,
  color: null,
  coverImageUrl: null,
  coverFocusY: 50,
  utilizationCategoryId: null,
  clientId: null,
  createdAt: new Date("2026-01-01"),
  updatedAt: new Date("2026-01-01"),
};

describe("project router", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe("configuration checks", () => {
    it("requires authentication for image generation configuration checks", async () => {
      const findUnique = vi.fn();
      const caller = createUnauthenticatedCaller({
        systemSettings: {
          findUnique,
        },
      });

      await expect(caller.isImageGenConfigured()).rejects.toMatchObject({
        code: "UNAUTHORIZED",
        message: "Authentication required",
      });
      await expect(caller.isDalleConfigured()).rejects.toMatchObject({
        code: "UNAUTHORIZED",
        message: "Authentication required",
      });

      expect(findUnique).not.toHaveBeenCalled();
    });

    it("returns only narrow readiness data for authenticated callers", async () => {
      const findUnique = vi.fn().mockResolvedValue({
        id: "singleton",
        imageProvider: "dalle",
      });
      const caller = createProtectedCaller({
        systemSettings: {
          findUnique,
        },
      });

      const imageGen = await caller.isImageGenConfigured();
      const dalle = await caller.isDalleConfigured();

      expect(imageGen).toEqual({
        configured: false,
        provider: "dalle",
      });
      expect(dalle).toEqual({
        configured: false,
      });
      expect(findUnique).toHaveBeenNthCalledWith(1, {
        where: { id: "singleton" },
      });
      expect(findUnique).toHaveBeenNthCalledWith(2, {
        where: { id: "singleton" },
      });
    });
  });

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

  describe("create", () => {
    it("creates a project and returns its id", async () => {
      const created = { ...sampleProject, id: "project_new" };
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue(null), // no shortCode conflict
          create: vi.fn().mockResolvedValue(created),
        },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
        webhook: { findMany: vi.fn().mockResolvedValue([]) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.create({
        shortCode: "PRJ-001",
        name: "Test Project",
        responsiblePerson: "Alice",
        orderType: OrderType.CHARGEABLE,
        allocationType: AllocationType.INT,
        winProbability: 80,
        budgetCents: 500_000_00,
        startDate: new Date("2026-01-01"),
        endDate: new Date("2026-06-30"),
      });

      expect(result.id).toBe("project_new");
      expect(db.project.create).toHaveBeenCalled();
      expect(db.auditLog.create).toHaveBeenCalled();
    });

    it("logs and swallows background cache and webhook failures during create", async () => {
      vi.mocked(invalidateDashboardCache).mockRejectedValueOnce(new Error("redis unavailable"));
      vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook unavailable"));

      const created = { ...sampleProject, id: "project_safe_create" };
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue(null),
          create: vi.fn().mockResolvedValue(created),
        },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
        webhook: { findMany: vi.fn().mockResolvedValue([]) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.create({
        shortCode: "SAFE-001",
        name: "Safe Project",
        responsiblePerson: "Alice",
        orderType: OrderType.CHARGEABLE,
        allocationType: AllocationType.INT,
        winProbability: 80,
        budgetCents: 500_000_00,
        startDate: new Date("2026-01-01"),
        endDate: new Date("2026-06-30"),
      });

      await Promise.resolve();
      await Promise.resolve();

      expect(result.id).toBe("project_safe_create");
      expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
        expect.objectContaining({ effectName: "invalidateDashboardCache" }),
        "Project background side effect failed",
      );
      expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
        expect.objectContaining({ effectName: "dispatchWebhooks", event: "project.created" }),
        "Project background side effect failed",
      );
    });

    it("throws CONFLICT when shortCode already exists", async () => {
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue(sampleProject),
          create: vi.fn(),
        },
        auditLog: { create: vi.fn() },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.create({
          shortCode: "PRJ-001",
          name: "Duplicate",
          responsiblePerson: "Alice",
          orderType: OrderType.CHARGEABLE,
          allocationType: AllocationType.INT,
          budgetCents: 100_00,
          startDate: new Date("2026-01-01"),
          endDate: new Date("2026-06-30"),
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "CONFLICT" }),
      );
    });

    it("blocks USER role from creating projects", async () => {
      const db = {
        project: { findUnique: vi.fn(), create: vi.fn() },
        auditLog: { create: vi.fn() },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.create({
          shortCode: "PRJ-002",
          name: "Blocked",
          responsiblePerson: "Alice",
          orderType: OrderType.CHARGEABLE,
          allocationType: AllocationType.INT,
          budgetCents: 100_00,
          startDate: new Date("2026-01-01"),
          endDate: new Date("2026-06-30"),
        }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });
  });

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

  describe("getById", () => {
    it("returns the correct project with allocations and demands for controller-level access", async () => {
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue({ ...sampleProject, blueprint: null }),
        },
        allocation: { findMany: vi.fn().mockResolvedValue([]) },
        demandRequirement: { findMany: vi.fn().mockResolvedValue([]) },
        assignment: { findMany: vi.fn().mockResolvedValue([]) },
      };

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

      expect(result.id).toBe("project_1");
      expect(result.name).toBe("Test Project");
      expect(result).toHaveProperty("allocations");
      expect(result).toHaveProperty("demands");
      expect(result).toHaveProperty("assignments");
    });

    it("throws NOT_FOUND when project does not exist", async () => {
      const db = {
        project: { findUnique: vi.fn().mockResolvedValue(null) },
        allocation: { findMany: vi.fn().mockResolvedValue([]) },
        demandRequirement: { findMany: vi.fn().mockResolvedValue([]) },
        assignment: { findMany: vi.fn().mockResolvedValue([]) },
      };

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

    it("blocks USER role from loading full project planning context", async () => {
      const db = {
        project: { findUnique: vi.fn() },
      };

      const caller = createProtectedCaller(db);
      await expect(caller.getById({ id: "project_1" })).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });
  });

  describe("getShoringRatio", () => {
    it("excludes regional holidays from shoring weighting", async () => {
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue({
            id: "project_1",
            name: "Test Project",
            shoringThreshold: 55,
            onshoreCountryCode: "DE",
          }),
        },
        assignment: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "a1",
              resourceId: "res_de",
              startDate: new Date("2026-01-05T00:00:00.000Z"),
              endDate: new Date("2026-01-06T00:00:00.000Z"),
              hoursPerDay: 8,
              resource: {
                id: "res_de",
                countryId: "country_de",
                federalState: "BY",
                metroCityId: null,
                availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0 },
                country: { id: "country_de", code: "DE" },
                metroCity: null,
              },
            },
            {
              id: "a2",
              resourceId: "res_es",
              startDate: new Date("2026-01-05T00:00:00.000Z"),
              endDate: new Date("2026-01-06T00:00:00.000Z"),
              hoursPerDay: 8,
              resource: {
                id: "res_es",
                countryId: "country_es",
                federalState: null,
                metroCityId: null,
                availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0 },
                country: { id: "country_es", code: "ES" },
                metroCity: null,
              },
            },
          ]),
        },
      };

      const caller = createControllerCaller(db);
      const result = await caller.getShoringRatio({ projectId: "project_1" });

      expect(result.totalHours).toBe(24);
      expect(result.onshoreRatio).toBe(33);
      expect(result.offshoreRatio).toBe(67);
    });
  });

  // ─── update ───────────────────────────────────────────────────────────────

  describe("update", () => {
    it("updates project fields", async () => {
      const updated = { ...sampleProject, name: "Updated Name" };
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue(sampleProject),
          update: vi.fn().mockResolvedValue(updated),
        },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.update({
        id: "project_1",
        data: { name: "Updated Name" },
      });

      expect(result.name).toBe("Updated Name");
      expect(db.project.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "project_1" },
        }),
      );
      expect(db.auditLog.create).toHaveBeenCalled();
    });

    it("throws NOT_FOUND when updating non-existent project", async () => {
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue(null),
          update: vi.fn(),
        },
        auditLog: { create: vi.fn() },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.update({ id: "missing", data: { name: "X" } }),
      ).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });
  });

  // ─── updateStatus ─────────────────────────────────────────────────────────

  describe("updateStatus", () => {
    it("transitions project status", async () => {
      const updated = { ...sampleProject, status: ProjectStatus.COMPLETED };
      const db = {
        project: {
          update: vi.fn().mockResolvedValue(updated),
        },
        webhook: { findMany: vi.fn().mockResolvedValue([]) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.updateStatus({
        id: "project_1",
        status: ProjectStatus.COMPLETED,
      });

      expect(result.status).toBe(ProjectStatus.COMPLETED);
      expect(db.project.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "project_1" },
          data: { status: ProjectStatus.COMPLETED },
        }),
      );
    });

    it("logs and swallows background failures during status changes", async () => {
      vi.mocked(invalidateDashboardCache).mockRejectedValueOnce(new Error("redis unavailable"));
      vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook unavailable"));

      const updated = { ...sampleProject, status: ProjectStatus.COMPLETED };
      const db = {
        project: {
          update: vi.fn().mockResolvedValue(updated),
        },
        webhook: { findMany: vi.fn().mockResolvedValue([]) },
      };

      const caller = createManagerCaller(db);
      const result = await caller.updateStatus({
        id: "project_1",
        status: ProjectStatus.COMPLETED,
      });

      await Promise.resolve();
      await Promise.resolve();

      expect(result.status).toBe(ProjectStatus.COMPLETED);
      expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
        expect.objectContaining({ effectName: "invalidateDashboardCache" }),
        "Project background side effect failed",
      );
      expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
        expect.objectContaining({ effectName: "dispatchWebhooks", event: "project.status_changed" }),
        "Project background side effect failed",
      );
    });
  });

  // ─── batchUpdateStatus ────────────────────────────────────────────────────

  describe("batchUpdateStatus", () => {
    it("updates multiple projects and returns count", async () => {
      const db = {
        project: {
          update: vi.fn().mockResolvedValue(sampleProject),
        },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
        $transaction: vi.fn((calls: unknown[]) =>
          Promise.all((calls as Promise<unknown>[]).map(() => sampleProject)),
        ),
      };

      const caller = createManagerCaller(db);
      const result = await caller.batchUpdateStatus({
        ids: ["project_1", "project_2", "project_3"],
        status: ProjectStatus.ON_HOLD,
      });

      expect(result.count).toBe(3);
      expect(db.auditLog.create).toHaveBeenCalled();
    });
  });

  // ─── delete ───────────────────────────────────────────────────────────────

  describe("delete", () => {
    it("deletes a project and cascades related records", async () => {
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue({ id: "project_1", name: "Test", shortCode: "PRJ" }),
          delete: vi.fn().mockResolvedValue({}),
        },
        assignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
        demandRequirement: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
        calculationRule: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createAdminCaller(db);
      const result = await caller.delete({ id: "project_1" });

      expect(result).toMatchObject({ id: "project_1", name: "Test" });
    });

    it("throws NOT_FOUND when deleting non-existent project", async () => {
      const db = {
        project: { findUnique: vi.fn().mockResolvedValue(null) },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createAdminCaller(db);
      await expect(caller.delete({ id: "missing" })).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });

    it("requires admin role — blocks manager", async () => {
      const db = {
        project: { findUnique: vi.fn() },
        $transaction: vi.fn(),
      };

      const caller = createManagerCaller(db);
      await expect(caller.delete({ id: "project_1" })).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });
  });

  // ─── batchDelete ──────────────────────────────────────────────────────────

  describe("batchDelete", () => {
    it("deletes multiple projects in a transaction", async () => {
      const projects = [
        { id: "p1", name: "A", shortCode: "A1" },
        { id: "p2", name: "B", shortCode: "B1" },
      ];
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue(projects),
          deleteMany: vi.fn().mockResolvedValue({ count: 2 }),
        },
        assignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
        demandRequirement: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
        calculationRule: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
        auditLog: { create: vi.fn().mockResolvedValue({}) },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createAdminCaller(db);
      const result = await caller.batchDelete({ ids: ["p1", "p2"] });

      expect(result.count).toBe(2);
    });

    it("throws NOT_FOUND when no projects match the ids", async () => {
      const db = {
        project: { findMany: vi.fn().mockResolvedValue([]) },
        $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
      };

      const caller = createAdminCaller(db);
      await expect(caller.batchDelete({ ids: ["missing"] })).rejects.toThrow(
        expect.objectContaining({ code: "NOT_FOUND" }),
      );
    });
  });

  // ─── listWithCosts ────────────────────────────────────────────────────────

  describe("listWithCosts", () => {
    it("returns projects with cost data", async () => {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([sampleProject]),
        },
      };

      const { listAssignmentBookings } = await import("@capakraken/application");
      vi.mocked(listAssignmentBookings).mockResolvedValue([]);

      const caller = createControllerCaller(db);
      const result = await caller.listWithCosts({ limit: 20 });

      expect(result.projects).toHaveLength(1);
      expect(result.projects[0]).toHaveProperty("totalCostCents");
      expect(result.projects[0]).toHaveProperty("totalPersonDays");
      expect(result.projects[0]).toHaveProperty("utilizationPercent");
    });

    it("calculates cost from assignment bookings", async () => {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([{ ...sampleProject, budgetCents: 100_000_00 }]),
        },
      };

      const { listAssignmentBookings } = await import("@capakraken/application");
      vi.mocked(listAssignmentBookings).mockResolvedValue([
        {
          id: "a1",
          projectId: "project_1",
          resourceId: "res_1",
          startDate: new Date("2026-01-01"),
          endDate: new Date("2026-01-05"),
          hoursPerDay: 8,
          dailyCostCents: 50000,
          status: "CONFIRMED",
          project: { id: "project_1", name: "Test", shortCode: "PRJ", status: "ACTIVE", orderType: "CHARGEABLE", dynamicFields: null },
          resource: { id: "res_1", displayName: "Alice", chapter: "CGI" },
        },
      ]);

      const caller = createControllerCaller(db);
      const result = await caller.listWithCosts({ limit: 20 });

      expect(result.projects[0]?.totalCostCents).toBeGreaterThan(0);
      expect(result.projects[0]?.totalPersonDays).toBeGreaterThan(0);
    });

    it("requires controller role — blocks USER", async () => {
      const db = { project: { findMany: vi.fn() } };

      const caller = createProtectedCaller(db);
      await expect(caller.listWithCosts({ limit: 20 })).rejects.toThrow(
        expect.objectContaining({ code: "FORBIDDEN" }),
      );
    });
  });

  describe("assistant-facing detail routes", () => {
    it("returns lightweight project search summaries from the canonical router", async () => {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "project_1",
              shortCode: "GDM",
              name: "Gelddruckmaschine",
              status: ProjectStatus.ACTIVE,
              startDate: new Date("2026-01-01T00:00:00.000Z"),
              endDate: new Date("2026-03-31T00:00:00.000Z"),
              client: { name: "Acme Mobility" },
            },
          ]),
        },
      };

      const caller = createControllerCaller(db);
      const result = await caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 });

      expect(result).toEqual([
        {
          id: "project_1",
          code: "GDM",
          name: "Gelddruckmaschine",
          status: "ACTIVE",
          start: "2026-01-01",
          end: "2026-03-31",
          client: "Acme Mobility",
        },
      ]);
    });

    it("returns formatted project search summaries from the canonical router", async () => {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "project_1",
              shortCode: "GDM",
              name: "Gelddruckmaschine",
              status: ProjectStatus.ACTIVE,
              budgetCents: 500000,
              winProbability: 100,
              startDate: new Date("2026-01-01T00:00:00.000Z"),
              endDate: new Date("2026-03-31T00:00:00.000Z"),
              client: { name: "Acme Mobility" },
              _count: { assignments: 3, estimates: 1 },
            },
          ]),
        },
      };

      const caller = createControllerCaller(db);
      const result = await caller.searchSummariesDetail({ search: "Gelddruckmaschine", limit: 10 });

      expect(result).toEqual([
        {
          id: "project_1",
          code: "GDM",
          name: "Gelddruckmaschine",
          status: "ACTIVE",
          budget: "5.000,00 EUR",
          winProbability: "100%",
          start: "2026-01-01",
          end: "2026-03-31",
          client: "Acme Mobility",
          assignmentCount: 3,
          estimateCount: 1,
        },
      ]);
    });

    it("blocks USER role from detailed project search summaries", async () => {
      const db = {
        project: {
          findMany: vi.fn(),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.searchSummariesDetail({ search: "Gelddruckmaschine", limit: 10 }),
      ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" }));
    });

    it("blocks USER role from lightweight project search summaries", async () => {
      const db = {
        project: {
          findMany: vi.fn(),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 }),
      ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" }));
    });

    it("allows explicit viewPlanning overrides to access lightweight project search summaries", async () => {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "project_1",
              shortCode: "GDM",
              name: "Gelddruckmaschine",
              status: ProjectStatus.ACTIVE,
              startDate: new Date("2026-01-01T00:00:00.000Z"),
              endDate: new Date("2026-03-31T00:00:00.000Z"),
              client: { name: "Acme Mobility" },
            },
          ]),
        },
      };

      const caller = createProtectedCallerWithOverrides(db, {
        granted: [PermissionKey.VIEW_PLANNING],
      });
      const result = await caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 });

      expect(result).toEqual([
        {
          id: "project_1",
          code: "GDM",
          name: "Gelddruckmaschine",
          status: "ACTIVE",
          start: "2026-01-01",
          end: "2026-03-31",
          client: "Acme Mobility",
        },
      ]);
    });

    it("does not treat viewCosts as a substitute for viewPlanning on lightweight project search summaries", async () => {
      const db = {
        project: {
          findMany: vi.fn(),
        },
      };

      const caller = createProtectedCallerWithOverrides(db, {
        granted: [PermissionKey.VIEW_COSTS],
      });
      await expect(
        caller.searchSummaries({ search: "Gelddruckmaschine", limit: 10 }),
      ).rejects.toThrow(
        expect.objectContaining({
          code: "FORBIDDEN",
          message: "Planning read access required",
        }),
      );
    });

    it("returns lightweight project identifier reads from the canonical router", async () => {
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce({
            id: "project_1",
            shortCode: "GDM",
            name: "Gelddruckmaschine",
            status: ProjectStatus.ACTIVE,
            startDate: new Date("2026-01-01T00:00:00.000Z"),
            endDate: new Date("2026-03-31T00:00:00.000Z"),
          }),
          findFirst: vi.fn(),
        },
      };

      const caller = createControllerCaller(db);
      const result = await caller.getByIdentifier({ identifier: "GDM" });

      expect(result).toEqual({
        id: "project_1",
        shortCode: "GDM",
        name: "Gelddruckmaschine",
        status: ProjectStatus.ACTIVE,
        startDate: new Date("2026-01-01T00:00:00.000Z"),
        endDate: new Date("2026-03-31T00:00:00.000Z"),
      });
    });

    it("returns formatted project details from the canonical router", async () => {
      const db = {
        project: {
          findUnique: vi.fn()
            .mockResolvedValueOnce(null)
            .mockResolvedValueOnce({
              id: "project_1",
              shortCode: "GDM",
              name: "Gelddruckmaschine",
              status: ProjectStatus.ACTIVE,
              startDate: new Date("2026-01-01T00:00:00.000Z"),
              endDate: new Date("2026-03-31T00:00:00.000Z"),
            })
            .mockResolvedValueOnce({
              id: "project_1",
              shortCode: "GDM",
              name: "Gelddruckmaschine",
              status: ProjectStatus.ACTIVE,
              orderType: OrderType.CHARGEABLE,
              allocationType: AllocationType.INT,
              budgetCents: 500000,
              winProbability: 100,
              startDate: new Date("2026-01-01T00:00:00.000Z"),
              endDate: new Date("2026-03-31T00:00:00.000Z"),
              responsiblePerson: "Bruce Banner",
              client: { name: "Acme Mobility" },
              utilizationCategory: { code: "BILLABLE", name: "Billable" },
              _count: { assignments: 3, estimates: 1 },
            }),
          findFirst: vi.fn(),
        },
        assignment: {
          findMany: vi.fn().mockResolvedValue([
            {
              resource: { displayName: "Bruce Banner", eid: "EMP-001" },
              role: "Lead",
              status: "ACTIVE",
              hoursPerDay: 8,
              startDate: new Date("2026-02-01T00:00:00.000Z"),
              endDate: new Date("2026-02-28T00:00:00.000Z"),
            },
          ]),
        },
      };

      const caller = createControllerCaller(db);
      const result = await caller.getByIdentifierDetail({ identifier: "GDM" });

      expect(result).toEqual({
        id: "project_1",
        code: "GDM",
        name: "Gelddruckmaschine",
        status: "ACTIVE",
        orderType: "CHARGEABLE",
        allocationType: "INT",
        budget: "5.000,00 EUR",
        budgetCents: 500000,
        winProbability: "100%",
        start: "2026-01-01",
        end: "2026-03-31",
        responsible: "Bruce Banner",
        client: "Acme Mobility",
        category: "Billable",
        assignmentCount: 3,
        estimateCount: 1,
        topAllocations: [
          {
            resource: "Bruce Banner",
            eid: "EMP-001",
            role: "Lead",
            status: "ACTIVE",
            hoursPerDay: 8,
            start: "2026-02-01",
            end: "2026-02-28",
          },
        ],
      });
    });

    it("blocks USER role from detailed project identifier reads", async () => {
      const db = {
        project: {
          findUnique: vi.fn(),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.getByIdentifierDetail({ identifier: "GDM" }),
      ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" }));
    });

    it("blocks USER role from lightweight project identifier reads", async () => {
      const db = {
        project: {
          findUnique: vi.fn(),
          findFirst: vi.fn(),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.getByIdentifier({ identifier: "GDM" }),
      ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" }));
    });
  });
});
