import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { describe, expect, it, vi } from "vitest";

vi.mock("@capakraken/application", () => ({
  countPlanningEntries: vi.fn(async (
    db: {
      demandRequirement: { findMany: (args?: { where?: { projectId?: { in: string[] }; roleId?: { in: string[] } } }) => Promise<Array<{ projectId: string; roleId: string | null }>> };
      assignment: { findMany: (args?: { where?: { projectId?: { in: string[] }; roleId?: { in: string[] } } }) => Promise<Array<{ projectId: string; roleId: string | null }>> };
    },
    input: { projectIds?: string[]; roleIds?: string[] } = {},
  ) => {
    const where = {
      ...(input.projectIds?.length ? { projectId: { in: input.projectIds } } : {}),
      ...(input.roleIds?.length ? { roleId: { in: input.roleIds } } : {}),
    };
    const [demandRequirements, assignments] = await Promise.all([
      db.demandRequirement.findMany({ where }),
      db.assignment.findMany({ where }),
    ]);

    const countsByProjectId = new Map<string, number>();
    const countsByRoleId = new Map<string, number>();

    for (const entry of [...demandRequirements, ...assignments]) {
      countsByProjectId.set(
        entry.projectId,
        (countsByProjectId.get(entry.projectId) ?? 0) + 1,
      );

      if (entry.roleId) {
        countsByRoleId.set(
          entry.roleId,
          (countsByRoleId.get(entry.roleId) ?? 0) + 1,
        );
      }
    }

    return {
      countsByProjectId,
      countsByRoleId,
      totalCount: demandRequirements.length + assignments.length,
    };
  }),
}));

import { roleRouter } from "../router/role.js";
import { createCallerFactory } from "../trpc.js";

vi.mock("../sse/event-bus.js", () => ({
  emitRoleCreated: vi.fn(),
  emitRoleDeleted: vi.fn(),
  emitRoleUpdated: vi.fn(),
}));

const createCaller = createCallerFactory(roleRouter);

function createManagerCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "manager@example.com", name: "Manager", image: null },
      expires: "2026-03-13T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_1",
      systemRole: SystemRole.MANAGER,
      permissionOverrides: 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: "2026-03-13T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_2",
      systemRole: SystemRole.USER,
      permissionOverrides: overrides,
    },
  });
}

describe("role router planning counts", () => {
  it("requires planning read access for role list views with planning counts", async () => {
    const caller = createProtectedCallerWithOverrides({}, null);

    await expect(caller.list({})).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Planning read access required",
    });

    await expect(caller.getByIdentifier({ identifier: "role_fx" })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Planning read access required",
    });

    await expect(caller.getById({ id: "role_fx" })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Planning read access required",
    });
  });

  it("does not treat viewCosts as a substitute for viewPlanning on role list views", async () => {
    const caller = createProtectedCallerWithOverrides({}, {
      granted: [PermissionKey.VIEW_COSTS],
    });

    await expect(caller.list({})).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Planning read access required",
    });
  });

  it("reports planning entry counts for roles", async () => {
    const db = {
      role: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "role_fx",
            name: "FX",
            description: null,
            color: "#111111",
            isActive: true,
            createdAt: new Date("2026-03-13"),
            updatedAt: new Date("2026-03-13"),
            _count: { resourceRoles: 2 },
          },
        ]),
      },
      allocation: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "legacy_demand",
            resourceId: null,
            projectId: "project_1",
            startDate: new Date("2026-03-17"),
            endDate: new Date("2026-03-18"),
            hoursPerDay: 8,
            percentage: 100,
            role: "FX",
            roleId: "role_fx",
            isPlaceholder: true,
            headcount: 2,
            dailyCostCents: 0,
            status: AllocationStatus.PROPOSED,
            metadata: {},
            createdAt: new Date("2026-03-13"),
            updatedAt: new Date("2026-03-13"),
          },
        ]),
      },
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "demand_1",
            projectId: "project_1",
            startDate: new Date("2026-03-17"),
            endDate: new Date("2026-03-18"),
            hoursPerDay: 8,
            percentage: 100,
            role: "FX",
            roleId: "role_fx",
            headcount: 2,
            status: AllocationStatus.PROPOSED,
            metadata: {},
            createdAt: new Date("2026-03-13"),
            updatedAt: new Date("2026-03-13"),
          },
        ]),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "assignment_1",
            demandRequirementId: null,
            resourceId: "resource_1",
            projectId: "project_1",
            startDate: new Date("2026-03-19"),
            endDate: new Date("2026-03-20"),
            hoursPerDay: 8,
            percentage: 100,
            role: "FX Lead",
            roleId: "role_fx",
            dailyCostCents: 32000,
            status: AllocationStatus.ACTIVE,
            metadata: {},
            createdAt: new Date("2026-03-13"),
            updatedAt: new Date("2026-03-13"),
          },
        ]),
      },
    };

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

    expect(result).toHaveLength(1);
    expect(result[0]?._count.resourceRoles).toBe(2);
    expect(result[0]?._count.allocations).toBe(2);
  });

  it("allows users with viewPlanning to load a role by identifier with planning counts", async () => {
    const db = {
      role: {
        findUnique: vi.fn().mockResolvedValue({
          id: "role_fx",
          name: "FX",
          description: null,
          color: "#111111",
          isActive: true,
          _count: { resourceRoles: 2 },
        }),
      },
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "assignment_1",
            demandRequirementId: null,
            resourceId: "resource_1",
            projectId: "project_1",
            startDate: new Date("2026-03-19"),
            endDate: new Date("2026-03-20"),
            hoursPerDay: 8,
            percentage: 100,
            role: "FX",
            roleId: "role_fx",
            dailyCostCents: 32000,
            status: AllocationStatus.ACTIVE,
            metadata: {},
            createdAt: new Date("2026-03-13"),
            updatedAt: new Date("2026-03-13"),
          },
        ]),
      },
    };

    const caller = createProtectedCallerWithOverrides(db, {
      granted: [PermissionKey.VIEW_PLANNING],
    });
    const result = await caller.getByIdentifier({ identifier: "role_fx" });

    expect(result._count.resourceRoles).toBe(2);
    expect(result._count.allocations).toBe(1);
    expect(db.role.findUnique).toHaveBeenCalledWith(expect.objectContaining({
      where: { id: "role_fx" },
      select: expect.objectContaining({
        id: true,
        name: true,
        description: true,
        color: true,
        isActive: true,
        _count: expect.any(Object),
      }),
    }));
  });

  it("blocks deleting a role that is only used by explicit demand or assignment rows", async () => {
    const db = {
      role: {
        findUnique: vi.fn().mockResolvedValue({
          id: "role_fx",
          name: "FX",
          description: null,
          color: "#111111",
          isActive: true,
          createdAt: new Date("2026-03-13"),
          updatedAt: new Date("2026-03-13"),
          _count: { resourceRoles: 0 },
        }),
        delete: vi.fn(),
      },
      allocation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "demand_1",
            projectId: "project_1",
            startDate: new Date("2026-03-17"),
            endDate: new Date("2026-03-18"),
            hoursPerDay: 8,
            percentage: 100,
            role: "FX",
            roleId: "role_fx",
            headcount: 1,
            status: AllocationStatus.PROPOSED,
            metadata: {},
            createdAt: new Date("2026-03-13"),
            updatedAt: new Date("2026-03-13"),
          },
        ]),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const caller = createManagerCaller(db);

    await expect(caller.delete({ id: "role_fx" })).rejects.toMatchObject({
      code: "PRECONDITION_FAILED",
    } satisfies Partial<TRPCError>);

    expect(db.role.delete).not.toHaveBeenCalled();
  });
});
