import { PermissionKey } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
  createRole,
  deactivateRole,
  deleteRole,
  getRoleById,
  getRoleByIdentifier,
  listRoles,
  resolveRoleByIdentifier,
  RoleIdInputSchema,
  RoleIdentifierInputSchema,
  RoleListInputSchema,
  ResolveRoleIdentifierInputSchema,
  UpdateRoleProcedureInputSchema,
  updateRole,
} from "../router/role-procedure-support.js";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";

const { countPlanningEntries } = vi.hoisted(() => ({
  countPlanningEntries: vi.fn(),
}));

const { emitRoleCreated, emitRoleDeleted, emitRoleUpdated } = vi.hoisted(() => ({
  emitRoleCreated: vi.fn(),
  emitRoleDeleted: vi.fn(),
  emitRoleUpdated: vi.fn(),
}));

vi.mock("@capakraken/application", () => ({
  countPlanningEntries,
}));

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

function createContext(db: Record<string, unknown>, permissions: PermissionKey[] = [PermissionKey.MANAGE_ROLES]) {
  return {
    db: db as never,
    dbUser: {
      id: "user_manager",
      systemRole: "MANAGER",
      permissionOverrides: null,
    },
    permissions: new Set(permissions),
  } as const;
}

describe("role procedure support", () => {
  beforeEach(() => {
    countPlanningEntries.mockReset();
    emitRoleCreated.mockReset();
    emitRoleDeleted.mockReset();
    emitRoleUpdated.mockReset();
  });

  it("lists roles with planning entry counts", async () => {
    countPlanningEntries.mockResolvedValue({
      countsByRoleId: new Map([["role_fx", 2]]),
    });
    const db = {
      role: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "role_fx",
            name: "FX",
            _count: { resourceRoles: 1 },
          },
        ]),
      },
      demandRequirement: {},
      assignment: {},
    };

    const result = await listRoles(
      createContext(db),
      RoleListInputSchema.parse({ search: "FX" }),
    );

    expect(result).toEqual([
      {
        id: "role_fx",
        name: "FX",
        _count: { resourceRoles: 1, allocations: 2 },
      },
    ]);
    expect(db.role.findMany).toHaveBeenCalledWith({
      where: { name: { contains: "FX", mode: "insensitive" } },
      include: { _count: { select: { resourceRoles: true } } },
      orderBy: { name: "asc" },
    });
  });

  it("resolves roles by identifier for protected read paths", async () => {
    const db = {
      role: {
        findUnique: vi.fn().mockResolvedValue({
          id: "role_fx",
          name: "FX",
          color: "#111111",
          isActive: true,
        }),
      },
    };

    const result = await resolveRoleByIdentifier(
      createContext(db),
      ResolveRoleIdentifierInputSchema.parse({ identifier: "role_fx" }),
    );

    expect(result).toEqual({
      id: "role_fx",
      name: "FX",
      color: "#111111",
      isActive: true,
    });
    expect(db.role.findUnique).toHaveBeenCalledWith({
      where: { id: "role_fx" },
      select: {
        id: true,
        name: true,
        color: true,
        isActive: true,
      },
    });
  });

  it("loads a role by identifier and attaches planning counts", async () => {
    countPlanningEntries.mockResolvedValue({
      countsByRoleId: new Map([["role_fx", 3]]),
    });
    const db = {
      role: {
        findUnique: vi.fn().mockResolvedValue({
          id: "role_fx",
          name: "FX",
          description: null,
          color: "#111111",
          isActive: true,
          _count: { resourceRoles: 2 },
        }),
      },
      demandRequirement: {},
      assignment: {},
    };

    const result = await getRoleByIdentifier(
      createContext(db),
      RoleIdentifierInputSchema.parse({ identifier: "role_fx" }),
    );

    expect(result).toEqual({
      id: "role_fx",
      name: "FX",
      description: null,
      color: "#111111",
      isActive: true,
      _count: { resourceRoles: 2, allocations: 3 },
    });
    expect(db.role.findUnique).toHaveBeenCalledWith({
      where: { id: "role_fx" },
      select: {
        id: true,
        name: true,
        description: true,
        color: true,
        isActive: true,
        _count: { select: { resourceRoles: true } },
      },
    });
  });

  it("loads a role by id with resource role details and planning counts", async () => {
    countPlanningEntries.mockResolvedValue({
      countsByRoleId: new Map([["role_fx", 1]]),
    });
    const db = {
      role: {
        findUnique: vi.fn().mockResolvedValue({
          id: "role_fx",
          name: "FX",
          _count: { resourceRoles: 1 },
          resourceRoles: [],
        }),
      },
      demandRequirement: {},
      assignment: {},
    };

    const result = await getRoleById(
      createContext(db),
      RoleIdInputSchema.parse({ id: "role_fx" }),
    );

    expect(result).toEqual({
      id: "role_fx",
      name: "FX",
      _count: { resourceRoles: 1, allocations: 1 },
      resourceRoles: [],
    });
    expect(db.role.findUnique).toHaveBeenCalledWith({
      where: { id: "role_fx" },
      include: {
        _count: { select: { resourceRoles: true } },
        resourceRoles: {
          include: {
            resource: { select: RESOURCE_BRIEF_SELECT },
          },
        },
      },
    });
  });

  it("creates roles with audit and zero allocation counts", async () => {
    const role = {
      id: "role_fx",
      name: "FX",
      description: null,
      color: "#111111",
      _count: { resourceRoles: 2 },
    };
    const db = {
      role: {
        findUnique: vi.fn().mockResolvedValue(null),
        create: vi.fn().mockResolvedValue(role),
      },
      auditLog: {
        create: vi.fn().mockResolvedValue(undefined),
      },
    };

    const result = await createRole(createContext(db), {
      name: "FX",
      description: undefined,
      color: "#111111",
    });

    expect(result).toEqual({
      ...role,
      _count: { resourceRoles: 2, allocations: 0 },
    });
    expect(db.role.create).toHaveBeenCalledWith({
      data: {
        name: "FX",
        description: null,
        color: "#111111",
      },
      include: { _count: { select: { resourceRoles: true } } },
    });
    expect(db.auditLog.create).toHaveBeenCalledWith({
      data: {
        entityType: "Role",
        entityId: "role_fx",
        action: "CREATE",
        changes: { after: role },
      },
    });
    expect(emitRoleCreated).toHaveBeenCalledWith({ id: "role_fx", name: "FX" });
  });

  it("requires manage roles permission for mutations", async () => {
    const ctx = createContext({
      role: {
        findUnique: vi.fn(),
      },
      auditLog: {
        create: vi.fn(),
      },
    }, []);

    await expect(createRole(ctx, {
      name: "FX",
      description: undefined,
      color: undefined,
    })).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });

  it("updates roles and emits the updated identifier payload", async () => {
    countPlanningEntries.mockResolvedValue({
      countsByRoleId: new Map([["role_fx", 3]]),
    });
    const existing = {
      id: "role_fx",
      name: "FX",
      description: "Old",
      color: "#111111",
      isActive: true,
      _count: { resourceRoles: 1 },
    };
    const updated = {
      id: "role_fx",
      name: "FX Lead",
      description: "Updated",
      color: "#222222",
      isActive: true,
      _count: { resourceRoles: 1 },
    };
    const db = {
      role: {
        findUnique: vi.fn()
          .mockResolvedValueOnce(existing)
          .mockResolvedValueOnce(null),
        update: vi.fn().mockResolvedValue(updated),
      },
      demandRequirement: {},
      assignment: {},
      auditLog: {
        create: vi.fn().mockResolvedValue(undefined),
      },
    };

    const result = await updateRole(createContext(db), UpdateRoleProcedureInputSchema.parse({
      id: "role_fx",
      data: {
        name: "FX Lead",
        description: "Updated",
        color: "#222222",
      },
    }));

    expect(result).toEqual({
      ...updated,
      _count: { resourceRoles: 1, allocations: 3 },
    });
    expect(db.role.update).toHaveBeenCalledWith({
      where: { id: "role_fx" },
      data: {
        name: "FX Lead",
        description: "Updated",
        color: "#222222",
      },
      include: { _count: { select: { resourceRoles: true } } },
    });
    expect(emitRoleUpdated).toHaveBeenCalledWith({ id: "role_fx", name: "FX Lead" });
  });

  it("blocks deletion when the role is still referenced", async () => {
    countPlanningEntries.mockResolvedValue({
      countsByRoleId: new Map([["role_fx", 2]]),
    });
    const db = {
      role: {
        findUnique: vi.fn().mockResolvedValue({
          id: "role_fx",
          name: "FX",
          _count: { resourceRoles: 1 },
        }),
        delete: vi.fn(),
      },
      demandRequirement: {},
      assignment: {},
      auditLog: {
        create: vi.fn(),
      },
    };

    await expect(deleteRole(createContext(db), RoleIdInputSchema.parse({ id: "role_fx" }))).rejects.toMatchObject({
      code: "PRECONDITION_FAILED",
      message: expect.stringContaining("Deactivate it instead"),
    });
    expect(db.role.delete).not.toHaveBeenCalled();
    expect(emitRoleDeleted).not.toHaveBeenCalled();
  });

  it("deactivates roles and preserves planning counts in the response", async () => {
    countPlanningEntries.mockResolvedValue({
      countsByRoleId: new Map([["role_fx", 4]]),
    });
    const db = {
      role: {
        update: vi.fn().mockResolvedValue({
          id: "role_fx",
          name: "FX",
          isActive: false,
          _count: { resourceRoles: 2 },
        }),
      },
      demandRequirement: {},
      assignment: {},
      auditLog: {
        create: vi.fn().mockResolvedValue(undefined),
      },
    };

    const result = await deactivateRole(createContext(db), RoleIdInputSchema.parse({ id: "role_fx" }));

    expect(result).toEqual({
      id: "role_fx",
      name: "FX",
      isActive: false,
      _count: { resourceRoles: 2, allocations: 4 },
    });
    expect(emitRoleUpdated).toHaveBeenCalledWith({ id: "role_fx", isActive: false });
  });
});
