import { describe, expect, it, vi } from "vitest";
import {
  approveVacation,
  batchApproveVacations,
  rejectVacation,
  batchRejectVacations,
  cancelVacation,
} from "../index.js";

// ---------------------------------------------------------------------------
// Shared fixtures
// ---------------------------------------------------------------------------

const baseVacation = {
  id: "vacation_1",
  resourceId: "resource_1",
  type: "ANNUAL" as const,
  startDate: new Date("2026-06-01"),
  endDate: new Date("2026-06-05"),
  isHalfDay: false,
  status: "PENDING" as const,
  requestedById: "user_1",
};

// ---------------------------------------------------------------------------
// approveVacation
// ---------------------------------------------------------------------------

describe("approveVacation", () => {
  function makeDb(vacation = baseVacation) {
    return {
      vacation: {
        findUnique: vi.fn().mockResolvedValue(vacation),
        update: vi.fn().mockResolvedValue({ ...vacation, status: "APPROVED" }),
      },
    };
  }

  function makeDeps() {
    return {
      assertVacationApprovable: vi.fn(),
      assertVacationStillChargeable: vi.fn().mockResolvedValue(undefined),
      buildVacationApprovalWriteData: vi.fn().mockResolvedValue({ deductionSnapshot: { days: 5 } }),
      checkVacationConflicts: vi.fn().mockResolvedValue({ warnings: [] }),
      buildApprovedVacationUpdateData: vi.fn().mockReturnValue({ status: "APPROVED" }),
    };
  }

  it("approves a PENDING vacation and returns the updated record", async () => {
    const db = makeDb();
    const deps = makeDeps();

    const result = await approveVacation(
      db as never,
      { id: "vacation_1", actorUserId: "manager_1" },
      deps,
    );

    expect(result.existingStatus).toBe("PENDING");
    expect(result.vacation.status).toBe("APPROVED");
    expect(result.warnings).toEqual([]);

    expect(deps.assertVacationApprovable).toHaveBeenCalledWith("PENDING");
    expect(deps.assertVacationStillChargeable).toHaveBeenCalledOnce();
    expect(deps.buildVacationApprovalWriteData).toHaveBeenCalledOnce();
    expect(deps.checkVacationConflicts).toHaveBeenCalledWith(db, "vacation_1", "manager_1");
    expect(db.vacation.update).toHaveBeenCalledWith({
      where: { id: "vacation_1" },
      data: { status: "APPROVED" },
    });
  });

  it("returns warnings from conflict check", async () => {
    const db = makeDb();
    const deps = makeDeps();
    deps.checkVacationConflicts.mockResolvedValue({
      warnings: ["Overlaps with another approval"],
    });

    const result = await approveVacation(db as never, { id: "vacation_1" }, deps);

    expect(result.warnings).toEqual(["Overlaps with another approval"]);
  });

  it("throws NOT_FOUND when vacation does not exist", async () => {
    const db = {
      vacation: { findUnique: vi.fn().mockResolvedValue(null) },
    };
    const deps = makeDeps();

    await expect(approveVacation(db as never, { id: "nonexistent" }, deps)).rejects.toMatchObject({
      code: "NOT_FOUND",
    });

    expect(deps.assertVacationApprovable).not.toHaveBeenCalled();
  });

  it("propagates error when assertVacationApprovable throws", async () => {
    const db = makeDb();
    const deps = makeDeps();
    deps.assertVacationApprovable.mockImplementation(() => {
      throw new Error("Status transition not allowed");
    });

    await expect(approveVacation(db as never, { id: "vacation_1" }, deps)).rejects.toThrow(
      "Status transition not allowed",
    );

    expect(db.vacation.update).not.toHaveBeenCalled();
  });

  it("propagates error when assertVacationStillChargeable throws", async () => {
    const db = makeDb();
    const deps = makeDeps();
    deps.assertVacationStillChargeable.mockRejectedValue(new Error("Insufficient balance"));

    await expect(approveVacation(db as never, { id: "vacation_1" }, deps)).rejects.toThrow(
      "Insufficient balance",
    );

    expect(db.vacation.update).not.toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// batchApproveVacations
// ---------------------------------------------------------------------------

describe("batchApproveVacations", () => {
  const pendingVacations = [
    { ...baseVacation, id: "vacation_1" },
    { ...baseVacation, id: "vacation_2", resourceId: "resource_2" },
  ];

  function makeDb(vacations = pendingVacations) {
    return {
      vacation: {
        findMany: vi.fn().mockResolvedValue(vacations),
        update: vi.fn().mockImplementation(({ where }) => {
          const found = vacations.find((v) => v.id === where.id)!;
          return Promise.resolve({ ...found, status: "APPROVED" });
        }),
      },
    };
  }

  function makeDeps() {
    return {
      assertVacationStillChargeable: vi.fn().mockResolvedValue(undefined),
      buildVacationApprovalWriteData: vi.fn().mockResolvedValue({ deductionSnapshot: {} }),
      checkBatchVacationConflicts: vi.fn().mockResolvedValue(new Map()),
      buildApprovedVacationUpdateData: vi.fn().mockReturnValue({ status: "APPROVED" }),
    };
  }

  it("approves all PENDING vacations in the batch", async () => {
    const db = makeDb();
    const deps = makeDeps();

    const result = await batchApproveVacations(
      db as never,
      { ids: ["vacation_1", "vacation_2"], actorUserId: "manager_1" },
      deps,
    );

    expect(result.approved).toBe(2);
    expect(result.updatedVacations).toHaveLength(2);
    expect(result.warnings).toEqual([]);
    expect(deps.assertVacationStillChargeable).toHaveBeenCalledTimes(2);
    expect(db.vacation.update).toHaveBeenCalledTimes(2);
  });

  it("collects warnings from batch conflict map", async () => {
    const db = makeDb();
    const deps = makeDeps();
    deps.checkBatchVacationConflicts.mockResolvedValue(
      new Map([
        ["vacation_1", ["Conflicts with leave policy"]],
        ["vacation_2", ["Overlapping vacation"]],
      ]),
    );

    const result = await batchApproveVacations(
      db as never,
      { ids: ["vacation_1", "vacation_2"] },
      deps,
    );

    expect(result.warnings).toEqual(["Conflicts with leave policy", "Overlapping vacation"]);
  });

  it("returns zero approved when no PENDING vacations found", async () => {
    const db = makeDb([]);
    const deps = makeDeps();

    const result = await batchApproveVacations(db as never, { ids: ["vacation_missing"] }, deps);

    expect(result.approved).toBe(0);
    expect(result.updatedVacations).toHaveLength(0);
    expect(deps.assertVacationStillChargeable).not.toHaveBeenCalled();
  });

  it("propagates error when assertVacationStillChargeable throws for any item", async () => {
    const db = makeDb();
    const deps = makeDeps();
    deps.assertVacationStillChargeable
      .mockResolvedValueOnce(undefined)
      .mockRejectedValueOnce(new Error("Insufficient balance for resource_2"));

    await expect(
      batchApproveVacations(db as never, { ids: ["vacation_1", "vacation_2"] }, deps),
    ).rejects.toThrow("Insufficient balance for resource_2");
  });
});

// ---------------------------------------------------------------------------
// rejectVacation
// ---------------------------------------------------------------------------

describe("rejectVacation", () => {
  function makeDb(vacation = baseVacation) {
    return {
      vacation: {
        findUnique: vi.fn().mockResolvedValue(vacation),
        update: vi.fn().mockResolvedValue({ ...vacation, status: "REJECTED" }),
      },
    };
  }

  function makeDeps() {
    return {
      assertVacationRejectable: vi.fn(),
      buildRejectedVacationUpdateData: vi.fn().mockReturnValue({ status: "REJECTED" }),
    };
  }

  it("rejects a PENDING vacation and returns the updated record", async () => {
    const db = makeDb();
    const deps = makeDeps();

    const result = await rejectVacation(
      db as never,
      { id: "vacation_1", rejectionReason: "No budget" },
      deps,
    );

    expect(result.vacation.status).toBe("REJECTED");
    expect(deps.assertVacationRejectable).toHaveBeenCalledWith("PENDING");
    expect(deps.buildRejectedVacationUpdateData).toHaveBeenCalledWith({
      rejectionReason: "No budget",
    });
    expect(db.vacation.update).toHaveBeenCalledWith({
      where: { id: "vacation_1" },
      data: { status: "REJECTED" },
    });
  });

  it("rejects without a reason when none provided", async () => {
    const db = makeDb();
    const deps = makeDeps();

    await rejectVacation(db as never, { id: "vacation_1" }, deps);

    expect(deps.buildRejectedVacationUpdateData).toHaveBeenCalledWith({
      rejectionReason: undefined,
    });
  });

  it("throws NOT_FOUND when vacation does not exist", async () => {
    const db = {
      vacation: { findUnique: vi.fn().mockResolvedValue(null) },
    };
    const deps = makeDeps();

    await expect(rejectVacation(db as never, { id: "nonexistent" }, deps)).rejects.toMatchObject({
      code: "NOT_FOUND",
    });

    expect(deps.assertVacationRejectable).not.toHaveBeenCalled();
  });

  it("propagates error when assertVacationRejectable throws", async () => {
    const db = makeDb();
    const deps = makeDeps();
    deps.assertVacationRejectable.mockImplementation(() => {
      throw new Error("Cannot reject already-approved vacation");
    });

    await expect(rejectVacation(db as never, { id: "vacation_1" }, deps)).rejects.toThrow(
      "Cannot reject already-approved vacation",
    );

    expect(db.vacation.update).not.toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// batchRejectVacations
// ---------------------------------------------------------------------------

describe("batchRejectVacations", () => {
  const pendingVacations = [
    { id: "vacation_1", resourceId: "resource_1" },
    { id: "vacation_2", resourceId: "resource_2" },
  ];

  function makeDb(vacations = pendingVacations) {
    return {
      vacation: {
        findMany: vi.fn().mockResolvedValue(vacations),
        updateMany: vi.fn().mockResolvedValue({ count: vacations.length }),
      },
    };
  }

  function makeDeps() {
    return {
      buildRejectedVacationUpdateData: vi.fn().mockReturnValue({ status: "REJECTED" }),
    };
  }

  it("rejects all PENDING vacations in the batch", async () => {
    const db = makeDb();
    const deps = makeDeps();

    const result = await batchRejectVacations(
      db as never,
      { ids: ["vacation_1", "vacation_2"], rejectionReason: "Budget freeze" },
      deps,
    );

    expect(result.rejected).toBe(2);
    expect(result.vacations).toEqual(pendingVacations);
    expect(db.vacation.updateMany).toHaveBeenCalledWith({
      where: { id: { in: ["vacation_1", "vacation_2"] } },
      data: { status: "REJECTED" },
    });
    expect(deps.buildRejectedVacationUpdateData).toHaveBeenCalledWith({
      rejectionReason: "Budget freeze",
    });
  });

  it("returns zero rejected when no PENDING vacations found", async () => {
    const db = makeDb([]);
    const deps = makeDeps();

    const result = await batchRejectVacations(db as never, { ids: ["vacation_missing"] }, deps);

    expect(result.rejected).toBe(0);
    expect(result.vacations).toHaveLength(0);
    expect(db.vacation.updateMany).toHaveBeenCalledWith({
      where: { id: { in: [] } },
      data: { status: "REJECTED" },
    });
  });
});

// ---------------------------------------------------------------------------
// cancelVacation
// ---------------------------------------------------------------------------

describe("cancelVacation", () => {
  function makeDb(
    vacation = baseVacation,
    resource: { userId: string } | null = { userId: "user_1" },
  ) {
    return {
      vacation: {
        findUnique: vi.fn().mockResolvedValue(vacation),
        update: vi.fn().mockResolvedValue({ ...vacation, status: "CANCELLED" }),
      },
      resource: {
        findUnique: vi.fn().mockResolvedValue(resource),
      },
    };
  }

  function makeDeps({
    cancelable = true,
    isManager = false,
    canCancel = true,
  }: {
    cancelable?: boolean;
    isManager?: boolean;
    canCancel?: boolean;
  } = {}) {
    return {
      assertVacationCancelable: vi.fn().mockImplementation(() => {
        if (!cancelable) throw new Error("Cannot cancel in current status");
      }),
      isVacationManagerRole: vi.fn().mockReturnValue(isManager),
      canActorCancelVacation: vi.fn().mockReturnValue(canCancel),
    };
  }

  it("allows the requester to cancel their own vacation", async () => {
    const db = makeDb();
    const deps = makeDeps({ isManager: false, canCancel: true });

    const result = await cancelVacation(
      db as never,
      { id: "vacation_1", actorId: "user_1", actorRole: "EMPLOYEE" },
      deps,
    );

    expect(result.existingStatus).toBe("PENDING");
    expect(result.vacation.status).toBe("CANCELLED");
    expect(deps.assertVacationCancelable).toHaveBeenCalledWith("PENDING");
    expect(db.vacation.update).toHaveBeenCalledWith({
      where: { id: "vacation_1" },
      data: { status: "CANCELLED" },
    });
  });

  it("allows a manager to cancel any vacation without resource lookup", async () => {
    const db = makeDb();
    const deps = makeDeps({ isManager: true, canCancel: true });

    // Manager is NOT the requester — resource lookup should be skipped.
    await cancelVacation(
      db as never,
      { id: "vacation_1", actorId: "manager_99", actorRole: "MANAGER" },
      deps,
    );

    // Resource lookup is skipped for manager role
    expect(db.resource.findUnique).not.toHaveBeenCalled();
    expect(db.vacation.update).toHaveBeenCalledOnce();
  });

  it("fetches resource and allows cancel when actor is resource owner", async () => {
    const vacation = { ...baseVacation, requestedById: "other_user" };
    const db = makeDb(vacation, { userId: "actor_user" });
    const deps = makeDeps({ isManager: false, canCancel: true });

    await cancelVacation(
      db as never,
      { id: "vacation_1", actorId: "actor_user", actorRole: "EMPLOYEE" },
      deps,
    );

    expect(db.resource.findUnique).toHaveBeenCalledWith({
      where: { id: vacation.resourceId },
      select: { userId: true },
    });
    expect(deps.canActorCancelVacation).toHaveBeenCalledWith({
      actorId: "actor_user",
      actorRole: "EMPLOYEE",
      requestedById: "other_user",
      resourceUserId: "actor_user",
    });
    expect(db.vacation.update).toHaveBeenCalledOnce();
  });

  it("throws NOT_FOUND when vacation does not exist", async () => {
    const db = {
      vacation: { findUnique: vi.fn().mockResolvedValue(null) },
      resource: { findUnique: vi.fn() },
    };
    const deps = makeDeps();

    await expect(
      cancelVacation(
        db as never,
        { id: "nonexistent", actorId: "user_1", actorRole: "EMPLOYEE" },
        deps,
      ),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });

    expect(deps.assertVacationCancelable).not.toHaveBeenCalled();
  });

  it("propagates error when assertVacationCancelable throws", async () => {
    const db = makeDb();
    const deps = makeDeps({ cancelable: false });

    await expect(
      cancelVacation(
        db as never,
        { id: "vacation_1", actorId: "user_1", actorRole: "EMPLOYEE" },
        deps,
      ),
    ).rejects.toThrow("Cannot cancel in current status");

    expect(db.vacation.update).not.toHaveBeenCalled();
  });

  it("throws FORBIDDEN when actor has no permission to cancel", async () => {
    // Actor is not manager, not the requester
    const vacation = { ...baseVacation, requestedById: "other_user" };
    const db = makeDb(vacation, { userId: "yet_another_user" });
    const deps = makeDeps({ isManager: false, canCancel: false });

    await expect(
      cancelVacation(
        db as never,
        { id: "vacation_1", actorId: "intruder", actorRole: "EMPLOYEE" },
        deps,
      ),
    ).rejects.toMatchObject({ code: "FORBIDDEN" });

    expect(db.vacation.update).not.toHaveBeenCalled();
  });

  it("skips resource lookup when actor is the requester (non-manager)", async () => {
    // requestedById matches actorId — no resource fetch needed
    const db = makeDb({ ...baseVacation, requestedById: "user_1" });
    const deps = makeDeps({ isManager: false, canCancel: true });

    await cancelVacation(
      db as never,
      { id: "vacation_1", actorId: "user_1", actorRole: "EMPLOYEE" },
      deps,
    );

    expect(db.resource.findUnique).not.toHaveBeenCalled();
  });
});
