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

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

const createCaller = createCallerFactory(resourceRouter);

beforeEach(() => {
  vi.clearAllMocks();
});

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

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

const VALID_CREATE_INPUT = {
  eid: "EMP-001",
  displayName: "Jane Doe",
  email: "jane@example.com",
  chapter: "Engineering",
  lcrCents: 5000,
  ucrCents: 8000,
  currency: "EUR",
  chargeabilityTarget: 80,
  availability: {
    monday: 8,
    tuesday: 8,
    wednesday: 8,
    thursday: 8,
    friday: 8,
  },
  skills: [],
  dynamicFields: {},
};

const MOCK_CREATED_RESOURCE = {
  id: "res_new",
  ...VALID_CREATE_INPUT,
  resourceRoles: [],
};

function mockDb(overrides: Record<string, unknown> = {}) {
  return {
    resource: {
      findFirst: vi.fn().mockResolvedValue(null),
      findUnique: vi.fn().mockResolvedValue(null),
      findMany: vi.fn().mockResolvedValue([]),
      update: vi.fn().mockResolvedValue({ id: "res_1", isActive: false }),
      delete: vi.fn().mockResolvedValue({}),
      deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    blueprint: {
      findUnique: vi.fn().mockResolvedValue(null),
      findMany: vi.fn().mockResolvedValue([]),
    },
    auditLog: {
      create: vi.fn().mockResolvedValue({}),
      createMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    assignment: {
      deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    vacation: {
      deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    resourceRole: {
      deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
      createMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => {
      const tx = {
        resource: {
          create: vi.fn().mockResolvedValue(MOCK_CREATED_RESOURCE),
          update: vi
            .fn()
            .mockResolvedValue({ id: "res_1", ...VALID_CREATE_INPUT, resourceRoles: [] }),
          delete: vi.fn().mockResolvedValue({}),
          deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        auditLog: {
          create: vi.fn().mockResolvedValue({}),
          createMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        resourceRole: {
          deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
          createMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        assignment: {
          deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        vacation: {
          deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
        },
        $executeRaw: vi.fn().mockResolvedValue(1),
      };
      return fn(tx);
    }),
    $executeRaw: vi.fn().mockResolvedValue(1),
    ...overrides,
  };
}

describe("resource create", () => {
  it("rejects non-manager users", async () => {
    const caller = createUserCaller(mockDb());
    await expect(caller.create(VALID_CREATE_INPUT)).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });

  it("rejects duplicate EID or email", async () => {
    const db = mockDb({
      resource: {
        findFirst: vi.fn().mockResolvedValue({ id: "existing", eid: "EMP-001" }),
      },
    });
    const caller = createManagerCaller(db);

    await expect(caller.create(VALID_CREATE_INPUT)).rejects.toMatchObject({
      code: "CONFLICT",
    });
  });

  it("rejects more than one primary role", async () => {
    const caller = createManagerCaller(mockDb());

    await expect(
      caller.create({
        ...VALID_CREATE_INPUT,
        roles: [
          { roleId: "role_1", isPrimary: true },
          { roleId: "role_2", isPrimary: true },
        ],
      }),
    ).rejects.toMatchObject({
      code: "BAD_REQUEST",
      message: expect.stringContaining("primary role"),
    });
  });

  it("creates resource with audit log for managers", async () => {
    const db = mockDb();
    const caller = createManagerCaller(db);

    const result = await caller.create(VALID_CREATE_INPUT);
    expect(result).toMatchObject({ id: "res_new" });
    expect(db.$transaction).toHaveBeenCalled();
  });
});

describe("resource update", () => {
  it("rejects non-manager users", async () => {
    const caller = createUserCaller(mockDb());
    await expect(
      caller.update({ id: "res_1", data: { displayName: "Updated" } }),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });

  it("throws NOT_FOUND for non-existent resource", async () => {
    const db = mockDb({
      resource: {
        ...mockDb().resource,
        findUnique: vi.fn().mockResolvedValue(null),
      },
    });
    const caller = createManagerCaller(db);

    await expect(
      caller.update({ id: "res_missing", data: { displayName: "Updated" } }),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });

  it("rejects multiple primary roles on update", async () => {
    const db = mockDb({
      resource: {
        ...mockDb().resource,
        findUnique: vi.fn().mockResolvedValue({
          id: "res_1",
          ...VALID_CREATE_INPUT,
          blueprintId: null,
          dynamicFields: {},
        }),
      },
    });
    const caller = createManagerCaller(db);

    await expect(
      caller.update({
        id: "res_1",
        data: {
          roles: [
            { roleId: "role_1", isPrimary: true },
            { roleId: "role_2", isPrimary: true },
          ],
        },
      }),
    ).rejects.toMatchObject({
      code: "BAD_REQUEST",
      message: expect.stringContaining("primary role"),
    });
  });
});

describe("resource deactivate", () => {
  it("rejects non-manager users", async () => {
    const caller = createUserCaller(mockDb());
    await expect(caller.deactivate({ id: "res_1" })).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });

  it("soft-deletes resource for managers", async () => {
    const db = mockDb();
    const caller = createManagerCaller(db);
    const result = await caller.deactivate({ id: "res_1" });
    expect(result).toBeDefined();
    expect(db.$transaction).toHaveBeenCalled();
  });
});

describe("resource batchUpdateCustomFields", () => {
  it("rejects non-manager users", async () => {
    const caller = createUserCaller(mockDb());
    await expect(
      caller.batchUpdateCustomFields({
        ids: ["res_1"],
        fields: { department: "Engineering" },
      }),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });

  it("validates field types (rejects invalid values)", async () => {
    const caller = createManagerCaller(mockDb());

    // The hardened schema only accepts string | number | boolean | null
    await expect(
      caller.batchUpdateCustomFields({
        ids: ["res_1"],
        // @ts-expect-error — intentionally passing an array to test schema validation
        fields: { department: ["nested", "array"] },
      }),
    ).rejects.toThrow();
  });

  it("executes batch update with audit log", async () => {
    const db = mockDb({
      resource: {
        findFirst: vi.fn().mockResolvedValue(null),
        findUnique: vi.fn().mockResolvedValue(null),
        findMany: vi.fn().mockResolvedValue([
          { id: "res_1", blueprintId: null },
          { id: "res_2", blueprintId: null },
        ]),
        update: vi.fn().mockResolvedValue({ id: "res_1", isActive: false }),
        delete: vi.fn().mockResolvedValue({}),
        deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
      },
      blueprint: {
        findUnique: vi.fn().mockResolvedValue(null),
        findMany: vi.fn().mockResolvedValue([
          {
            fieldDefs: [
              { key: "department", label: "Department", type: "text" },
              { key: "level", label: "Level", type: "number" },
            ],
          },
        ]),
      },
    });
    const caller = createManagerCaller(db);

    const result = await caller.batchUpdateCustomFields({
      ids: ["res_1", "res_2"],
      fields: { department: "Engineering", level: 3 },
    });

    expect(result).toEqual({ updated: 2 });
    expect(db.$transaction).toHaveBeenCalled();
  });

  it("rejects unknown keys when a blueprint defines the whitelist", async () => {
    const db = mockDb({
      resource: {
        findFirst: vi.fn().mockResolvedValue(null),
        findUnique: vi.fn().mockResolvedValue(null),
        findMany: vi.fn().mockResolvedValue([{ id: "res_1", blueprintId: "bp_1" }]),
        update: vi.fn().mockResolvedValue({}),
        delete: vi.fn().mockResolvedValue({}),
        deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
      },
      blueprint: {
        findUnique: vi.fn().mockResolvedValue({
          target: "RESOURCE",
          fieldDefs: [{ key: "department", label: "Department", type: "text" }],
        }),
        findMany: vi.fn().mockResolvedValue([]),
      },
    });
    const caller = createManagerCaller(db);

    await expect(
      caller.batchUpdateCustomFields({
        ids: ["res_1"],
        // "injected" is not in the blueprint's whitelist
        fields: { department: "Engineering", injected: "malicious" },
      }),
    ).rejects.toThrow();
    expect(db.$transaction).not.toHaveBeenCalled();
  });

  it("404s if any requested id does not exist", async () => {
    const db = mockDb({
      resource: {
        findFirst: vi.fn().mockResolvedValue(null),
        findUnique: vi.fn().mockResolvedValue(null),
        findMany: vi.fn().mockResolvedValue([{ id: "res_1", blueprintId: null }]),
        update: vi.fn().mockResolvedValue({}),
        delete: vi.fn().mockResolvedValue({}),
        deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
      },
    });
    const caller = createManagerCaller(db);

    await expect(
      caller.batchUpdateCustomFields({
        ids: ["res_1", "res_missing"],
        fields: { department: "Engineering" },
      }),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });
});

describe("resource hardDelete", () => {
  it("rejects non-admin users", async () => {
    const caller = createManagerCaller(mockDb());
    await expect(caller.hardDelete({ id: "res_1" })).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });

  it("throws NOT_FOUND for missing resource", async () => {
    const db = mockDb({
      resource: {
        ...mockDb().resource,
        findUnique: vi.fn().mockResolvedValue(null),
      },
    });
    const caller = createAdminCaller(db);

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

  it("deletes resource and cascades for admin", async () => {
    const db = mockDb({
      resource: {
        ...mockDb().resource,
        findUnique: vi.fn().mockResolvedValue({ id: "res_1", displayName: "Jane", eid: "EMP-001" }),
      },
    });
    const caller = createAdminCaller(db);

    const result = await caller.hardDelete({ id: "res_1" });
    expect(result).toEqual({ deleted: true });
    expect(db.$transaction).toHaveBeenCalled();
  });
});
