import { SystemRole } from "@nexus/shared";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { calculationRuleRouter } from "../router/calculation-rules.js";
import { createCallerFactory } from "../trpc.js";

// ─── Mocks ────────────────────────────────────────────────────────────────────

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

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

const createCaller = createCallerFactory(calculationRuleRouter);

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

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

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

// ─── Fixtures ─────────────────────────────────────────────────────────────────

const ruleFixture = {
  id: "rule_1",
  name: "Sick Leave Rule",
  description: "Apply zero cost during sick leave",
  triggerType: "SICK",
  projectId: null,
  orderType: null,
  costEffect: "ZERO",
  costReductionPercent: null,
  chargeabilityEffect: "SKIP",
  priority: 100,
  isActive: true,
};

const ruleWithProjectFixture = {
  ...ruleFixture,
  project: { id: "proj_1", name: "Big Feature", code: "BF-2025" },
};

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

describe("calculationRule.list", () => {
  it("returns all rules ordered by priority descending then name", async () => {
    const findMany = vi.fn().mockResolvedValue([ruleWithProjectFixture]);
    const caller = createAdminCaller({ calculationRule: { findMany } });

    const result = await caller.list();

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        orderBy: [{ priority: "desc" }, { name: "asc" }],
        include: expect.objectContaining({ project: expect.anything() }),
      }),
    );
    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({ id: "rule_1", name: "Sick Leave Rule" });
  });

  it("returns an empty list when no rules are defined", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createAdminCaller({ calculationRule: { findMany } });

    const result = await caller.list();

    expect(result).toHaveLength(0);
  });

  it("allows CONTROLLER role to list rules", async () => {
    const findMany = vi.fn().mockResolvedValue([ruleWithProjectFixture]);
    const caller = createControllerCaller({ calculationRule: { findMany } });

    const result = await caller.list();

    expect(result).toHaveLength(1);
  });

  it("rejects VIEWER role with FORBIDDEN", async () => {
    const caller = createViewerCaller({ calculationRule: { findMany: vi.fn() } });

    await expect(caller.list()).rejects.toMatchObject({ code: "FORBIDDEN" });
  });
});

describe("calculationRule.getById", () => {
  it("returns the rule with project include when it exists", async () => {
    const findUnique = vi.fn().mockResolvedValue(ruleWithProjectFixture);
    const caller = createAdminCaller({ calculationRule: { findUnique } });

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

    expect(findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "rule_1" } }));
    expect(result).toMatchObject({ id: "rule_1", name: "Sick Leave Rule" });
  });

  it("throws NOT_FOUND when the rule does not exist", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);
    const caller = createAdminCaller({ calculationRule: { findUnique } });

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

describe("calculationRule.getActive", () => {
  it("returns only active rules without project include", async () => {
    const activeRule = { ...ruleFixture };
    const findMany = vi.fn().mockResolvedValue([activeRule]);
    const caller = createAdminCaller({ calculationRule: { findMany } });

    const result = await caller.getActive();

    expect(findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { isActive: true },
        orderBy: [{ priority: "desc" }],
      }),
    );
    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({ id: "rule_1", isActive: true });
  });

  it("returns an empty list when no active rules exist", async () => {
    const findMany = vi.fn().mockResolvedValue([]);
    const caller = createAdminCaller({ calculationRule: { findMany } });

    const result = await caller.getActive();

    expect(result).toHaveLength(0);
  });
});

describe("calculationRule.create", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("creates a rule with required fields and returns it", async () => {
    const created = { ...ruleFixture };
    const dbCreate = vi.fn().mockResolvedValue(created);
    const caller = createManagerCaller({ calculationRule: { create: dbCreate } });

    const result = await caller.create({
      name: "Sick Leave Rule",
      triggerType: "SICK",
      costEffect: "ZERO",
      chargeabilityEffect: "SKIP",
      priority: 100,
      isActive: true,
    });

    expect(dbCreate).toHaveBeenCalled();
    expect(result).toMatchObject({
      id: "rule_1",
      name: "Sick Leave Rule",
      triggerType: "SICK",
      costEffect: "ZERO",
    });
  });

  it("creates a rule with optional description and projectId", async () => {
    const created = {
      ...ruleFixture,
      description: "Project-scoped rule",
      projectId: "proj_1",
      costEffect: "REDUCE",
      costReductionPercent: 50,
    };
    const dbCreate = vi.fn().mockResolvedValue(created);
    const caller = createManagerCaller({ calculationRule: { create: dbCreate } });

    const result = await caller.create({
      name: "Sick Leave Rule",
      description: "Project-scoped rule",
      triggerType: "SICK",
      projectId: "proj_1",
      costEffect: "REDUCE",
      costReductionPercent: 50,
      chargeabilityEffect: "SKIP",
      priority: 50,
      isActive: true,
    });

    expect(result).toMatchObject({
      description: "Project-scoped rule",
      projectId: "proj_1",
      costReductionPercent: 50,
    });
  });

  it("rejects CONTROLLER role with FORBIDDEN (requires MANAGER or ADMIN)", async () => {
    const caller = createControllerCaller({ calculationRule: { create: vi.fn() } });

    await expect(
      caller.create({
        name: "Test",
        triggerType: "VACATION",
        costEffect: "CHARGE",
        chargeabilityEffect: "COUNT",
        priority: 0,
        isActive: true,
      }),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });
  });

  it("rejects invalid triggerType with a validation error", async () => {
    const caller = createManagerCaller({ calculationRule: { create: vi.fn() } });

    await expect(
      caller.create({
        name: "Bad Rule",
        triggerType: "UNKNOWN_TRIGGER" as never,
        costEffect: "ZERO",
        chargeabilityEffect: "SKIP",
        priority: 0,
        isActive: true,
      }),
    ).rejects.toThrow();
  });
});

describe("calculationRule.update", () => {
  it("updates a rule name and returns the updated record", async () => {
    const updated = { ...ruleFixture, name: "Updated Sick Rule" };
    const findUnique = vi.fn().mockResolvedValue(ruleFixture);
    const update = vi.fn().mockResolvedValue(updated);
    const caller = createManagerCaller({ calculationRule: { findUnique, update } });

    const result = await caller.update({ id: "rule_1", name: "Updated Sick Rule" });

    expect(update).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "rule_1" } }));
    expect(result).toMatchObject({ name: "Updated Sick Rule" });
  });

  it("throws NOT_FOUND when the rule to update does not exist", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);
    const caller = createManagerCaller({ calculationRule: { findUnique } });

    await expect(caller.update({ id: "missing", name: "Ghost Rule" })).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("deactivates a rule via isActive: false", async () => {
    const updated = { ...ruleFixture, isActive: false };
    const findUnique = vi.fn().mockResolvedValue(ruleFixture);
    const update = vi.fn().mockResolvedValue(updated);
    const caller = createManagerCaller({ calculationRule: { findUnique, update } });

    const result = await caller.update({ id: "rule_1", isActive: false });

    expect(result).toMatchObject({ isActive: false });
  });
});

describe("calculationRule.delete", () => {
  it("deletes the rule and returns success", async () => {
    const findUnique = vi.fn().mockResolvedValue(ruleFixture);
    const dbDelete = vi.fn().mockResolvedValue(undefined);
    const caller = createManagerCaller({ calculationRule: { findUnique, delete: dbDelete } });

    const result = await caller.delete({ id: "rule_1" });

    expect(dbDelete).toHaveBeenCalledWith({ where: { id: "rule_1" } });
    expect(result).toMatchObject({ success: true });
  });

  it("throws NOT_FOUND when the rule does not exist", async () => {
    const findUnique = vi.fn().mockResolvedValue(null);
    const caller = createManagerCaller({ calculationRule: { findUnique } });

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

  it("rejects CONTROLLER role with FORBIDDEN", async () => {
    const caller = createControllerCaller({ calculationRule: { findUnique: vi.fn() } });

    await expect(caller.delete({ id: "rule_1" })).rejects.toMatchObject({
      code: "FORBIDDEN",
    });
  });
});
