import { SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { roleRouter } from "../router/role.js";
import { createCallerFactory } from "../trpc.js";

// ─── Hoisted mocks ────────────────────────────────────────────────────────────

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,
}));

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

// ─── Caller factory ───────────────────────────────────────────────────────────

const createCaller = createCallerFactory(roleRouter);

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: "user_admin",
      systemRole: SystemRole.ADMIN,
      permissionOverrides: null,
    },
    roleDefaults: null,
  });
}

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

// ─── Helpers ──────────────────────────────────────────────────────────────────

function mockPlanningCounts(roleId: string, count = 0) {
  countPlanningEntries.mockResolvedValue({
    countsByRoleId: new Map([[roleId, count]]),
  });
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe("role.list", () => {
  beforeEach(() => {
    countPlanningEntries.mockReset();
  });

  it("returns all roles ordered by name with allocation counts", async () => {
    countPlanningEntries.mockResolvedValue({
      countsByRoleId: new Map([
        ["role_fx", 3],
        ["role_td", 1],
      ]),
    });

    const findMany = vi.fn().mockResolvedValue([
      { id: "role_fx", name: "FX Artist", _count: { resourceRoles: 2 } },
      { id: "role_td", name: "Technical Director", _count: { resourceRoles: 5 } },
    ]);

    const caller = createAdminCaller({
      role: { findMany },
      demandRequirement: {},
      assignment: {},
    });

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

    expect(findMany).toHaveBeenCalledWith({
      where: {},
      include: { _count: { select: { resourceRoles: true } } },
      orderBy: { name: "asc" },
    });
    expect(result).toHaveLength(2);
    expect(result[0]).toMatchObject({
      id: "role_fx",
      _count: { resourceRoles: 2, allocations: 3 },
    });
    expect(result[1]).toMatchObject({
      id: "role_td",
      _count: { resourceRoles: 5, allocations: 1 },
    });
  });

  it("filters by isActive when supplied", async () => {
    countPlanningEntries.mockResolvedValue({ countsByRoleId: new Map() });

    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createAdminCaller({
      role: { findMany },
      demandRequirement: {},
      assignment: {},
    });

    await caller.list({ isActive: true });

    expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { isActive: true } }));
  });

  it("filters by search string using case-insensitive contains", async () => {
    countPlanningEntries.mockResolvedValue({ countsByRoleId: new Map() });

    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createAdminCaller({
      role: { findMany },
      demandRequirement: {},
      assignment: {},
    });

    await caller.list({ search: "model" });

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { name: { contains: "model", mode: "insensitive" } },
      }),
    );
  });

  it("assigns zero allocation count for roles not present in the planning counts map", async () => {
    countPlanningEntries.mockResolvedValue({ countsByRoleId: new Map() });

    const findMany = vi
      .fn()
      .mockResolvedValue([{ id: "role_new", name: "New Role", _count: { resourceRoles: 0 } }]);
    const caller = createAdminCaller({
      role: { findMany },
      demandRequirement: {},
      assignment: {},
    });

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

    expect(result[0]!._count.allocations).toBe(0);
  });
});

describe("role.getById", () => {
  beforeEach(() => {
    countPlanningEntries.mockReset();
  });

  it("returns the role with resources and allocation count", async () => {
    mockPlanningCounts("role_1", 4);

    const findUnique = vi.fn().mockResolvedValue({
      id: "role_1",
      name: "Compositor",
      description: "Comp work",
      color: "#ff0000",
      isActive: true,
      _count: { resourceRoles: 2 },
      resourceRoles: [],
    });

    const caller = createAdminCaller({
      role: { findUnique },
      demandRequirement: {},
      assignment: {},
    });

    const result = await caller.getById({ id: "role_1" });

    expect(findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "role_1" } }));
    expect(result).toMatchObject({
      id: "role_1",
      name: "Compositor",
      _count: { resourceRoles: 2, allocations: 4 },
    });
  });

  it("throws NOT_FOUND when the role does not exist", async () => {
    countPlanningEntries.mockResolvedValue({ countsByRoleId: new Map() });

    const findUnique = vi.fn().mockResolvedValue(null);
    const caller = createAdminCaller({
      role: { findUnique },
      demandRequirement: {},
      assignment: {},
    });

    await expect(caller.getById({ id: "missing_role" })).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("returns role with zero allocation count when no planning entries exist", async () => {
    countPlanningEntries.mockResolvedValue({ countsByRoleId: new Map() });

    const findUnique = vi.fn().mockResolvedValue({
      id: "role_2",
      name: "Rigger",
      description: null,
      color: null,
      isActive: true,
      _count: { resourceRoles: 0 },
      resourceRoles: [],
    });

    const caller = createAdminCaller({
      role: { findUnique },
      demandRequirement: {},
      assignment: {},
    });

    const result = await caller.getById({ id: "role_2" });

    expect(result._count.allocations).toBe(0);
  });

  it("includes linked resources in the response", async () => {
    mockPlanningCounts("role_3", 1);

    const resource = { id: "res_1", displayName: "Alice", eid: "A001", lcrCents: 0, chapter: null };
    const findUnique = vi.fn().mockResolvedValue({
      id: "role_3",
      name: "Animator",
      description: null,
      color: "#00ff00",
      isActive: true,
      _count: { resourceRoles: 1 },
      resourceRoles: [{ resource }],
    });

    const caller = createAdminCaller({
      role: { findUnique },
      demandRequirement: {},
      assignment: {},
    });

    const result = await caller.getById({ id: "role_3" });

    expect(result.resourceRoles).toHaveLength(1);
    expect(result.resourceRoles[0]!.resource).toMatchObject({ id: "res_1", displayName: "Alice" });
  });
});

describe("role.create", () => {
  beforeEach(() => {
    countPlanningEntries.mockReset();
    emitRoleCreated.mockReset();
  });

  it("creates a role with required name only and returns it with zero counts", async () => {
    const created = {
      id: "role_new_1",
      name: "Layout Artist",
      description: null,
      color: null,
      isActive: true,
      _count: { resourceRoles: 0 },
    };

    const roleFindUnique = vi.fn().mockResolvedValue(null); // name availability check
    const transaction = vi.fn().mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) =>
      fn({
        role: {
          create: vi.fn().mockResolvedValue(created),
        },
        auditLog: { create: vi.fn().mockResolvedValue(undefined) },
      }),
    );

    const caller = createManagerCaller({
      role: { findUnique: roleFindUnique },
      $transaction: transaction,
      demandRequirement: {},
      assignment: {},
    });

    const result = await caller.create({ name: "Layout Artist" });

    expect(result).toMatchObject({
      id: "role_new_1",
      name: "Layout Artist",
      _count: { resourceRoles: 0, allocations: 0 },
    });
    expect(emitRoleCreated).toHaveBeenCalledWith({ id: "role_new_1", name: "Layout Artist" });
  });

  it("creates a role with optional color and description", async () => {
    const created = {
      id: "role_new_2",
      name: "VFX Supervisor",
      description: "Oversees VFX pipeline",
      color: "#aabbcc",
      isActive: true,
      _count: { resourceRoles: 0 },
    };

    const roleFindUnique = vi.fn().mockResolvedValue(null);
    const transaction = vi.fn().mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) =>
      fn({
        role: {
          create: vi.fn().mockResolvedValue(created),
        },
        auditLog: { create: vi.fn().mockResolvedValue(undefined) },
      }),
    );

    const caller = createManagerCaller({
      role: { findUnique: roleFindUnique },
      $transaction: transaction,
      demandRequirement: {},
      assignment: {},
    });

    const result = await caller.create({
      name: "VFX Supervisor",
      description: "Oversees VFX pipeline",
      color: "#aabbcc",
    });

    expect(result).toMatchObject({
      name: "VFX Supervisor",
      description: "Oversees VFX pipeline",
      color: "#aabbcc",
    });
  });

  it("throws CONFLICT when a role with the same name already exists", async () => {
    const roleFindUnique = vi.fn().mockResolvedValue({ id: "role_existing", name: "FX Artist" });

    const caller = createManagerCaller({
      role: { findUnique: roleFindUnique },
      demandRequirement: {},
      assignment: {},
    });

    await expect(caller.create({ name: "FX Artist" })).rejects.toMatchObject({
      code: "CONFLICT",
      message: 'Role "FX Artist" already exists',
    });
  });

  it("rejects creation when the caller does not have MANAGER or ADMIN role", async () => {
    const viewerCaller = createCaller({
      session: {
        user: { email: "viewer@example.com", name: "Viewer", image: null },
        expires: "2099-01-01T00:00:00.000Z",
      },
      db: {} as never,
      dbUser: {
        id: "user_viewer",
        systemRole: SystemRole.VIEWER,
        permissionOverrides: null,
      },
      roleDefaults: null,
    });

    await expect(viewerCaller.create({ name: "New Role" })).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });
});

describe("role.update", () => {
  beforeEach(() => {
    countPlanningEntries.mockReset();
    emitRoleUpdated.mockReset();
  });

  it("updates the role name when a new unique name is provided", async () => {
    mockPlanningCounts("role_1", 0);

    const existing = {
      id: "role_1",
      name: "Animator",
      description: null,
      color: null,
      isActive: true,
    };
    const updated = { ...existing, name: "Senior Animator", _count: { resourceRoles: 1 } };

    const roleFindUnique = vi
      .fn()
      .mockResolvedValueOnce(existing) // fetch existing for update guard
      .mockResolvedValueOnce(null); // name availability check

    const transaction = vi.fn().mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) =>
      fn({
        role: { update: vi.fn().mockResolvedValue(updated) },
        auditLog: { create: vi.fn().mockResolvedValue(undefined) },
      }),
    );

    const caller = createManagerCaller({
      role: { findUnique: roleFindUnique },
      $transaction: transaction,
      demandRequirement: {},
      assignment: {},
    });

    const result = await caller.update({ id: "role_1", data: { name: "Senior Animator" } });

    expect(result).toMatchObject({ id: "role_1", name: "Senior Animator" });
    expect(emitRoleUpdated).toHaveBeenCalledWith({ id: "role_1", name: "Senior Animator" });
  });

  it("updates color and description without renaming the role", async () => {
    mockPlanningCounts("role_2", 2);

    const existing = {
      id: "role_2",
      name: "Rigger",
      description: null,
      color: null,
      isActive: true,
    };
    const updated = {
      ...existing,
      description: "Character rigging",
      color: "#123456",
      _count: { resourceRoles: 0 },
    };

    const roleFindUnique = vi.fn().mockResolvedValueOnce(existing);

    const transaction = vi.fn().mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) =>
      fn({
        role: { update: vi.fn().mockResolvedValue(updated) },
        auditLog: { create: vi.fn().mockResolvedValue(undefined) },
      }),
    );

    const caller = createManagerCaller({
      role: { findUnique: roleFindUnique },
      $transaction: transaction,
      demandRequirement: {},
      assignment: {},
    });

    const result = await caller.update({
      id: "role_2",
      data: { description: "Character rigging", color: "#123456" },
    });

    expect(result).toMatchObject({ description: "Character rigging", color: "#123456" });
  });

  it("throws NOT_FOUND when the target role does not exist", async () => {
    const roleFindUnique = vi.fn().mockResolvedValue(null);

    const caller = createManagerCaller({
      role: { findUnique: roleFindUnique },
      demandRequirement: {},
      assignment: {},
    });

    await expect(
      caller.update({ id: "missing_role", data: { name: "Something" } }),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });

  it("throws CONFLICT when renaming to an existing role name", async () => {
    const existing = {
      id: "role_1",
      name: "Animator",
      description: null,
      color: null,
      isActive: true,
    };
    const conflicting = { id: "role_99", name: "Senior Animator" };

    const roleFindUnique = vi
      .fn()
      .mockResolvedValueOnce(existing) // existing check
      .mockResolvedValueOnce(conflicting); // name availability check

    const caller = createManagerCaller({
      role: { findUnique: roleFindUnique },
      demandRequirement: {},
      assignment: {},
    });

    await expect(
      caller.update({ id: "role_1", data: { name: "Senior Animator" } }),
    ).rejects.toMatchObject({ code: "CONFLICT" });
  });
});
