import { SystemRole } from "@capakraken/shared";
import { VacationStatus, VacationType } from "@capakraken/db";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { createNotification } from "../lib/create-notification.js";
import { logger } from "../lib/logger.js";
import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
import { vacationRouter } from "../router/vacation.js";
import { createCallerFactory } from "../trpc.js";

vi.mock("../sse/event-bus.js", () => ({
  emitVacationCreated: vi.fn(),
  emitVacationUpdated: vi.fn(),
  emitVacationDeleted: vi.fn(),
  emitNotificationCreated: vi.fn(),
  emitTaskAssigned: vi.fn(),
}));

vi.mock("../lib/email.js", () => ({
  sendEmail: vi.fn(),
}));

vi.mock("../lib/create-notification.js", () => ({
  createNotification: vi.fn().mockResolvedValue("notif_1"),
}));

vi.mock("../lib/vacation-conflicts.js", () => ({
  checkVacationConflicts: vi.fn().mockResolvedValue({ warnings: [] }),
  checkBatchVacationConflicts: vi.fn().mockResolvedValue(new Map()),
}));

vi.mock("../lib/webhook-dispatcher.js", () => ({
  dispatchWebhooks: vi.fn().mockResolvedValue(undefined),
}));

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

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

const createCaller = createCallerFactory(vacationRouter);

function createProtectedCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "user@example.com", name: "User", image: null },
      expires: "2026-12-31T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "user_1",
      systemRole: SystemRole.USER,
      permissionOverrides: null,
    },
  });
}

function createManagerCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "manager@example.com", name: "Manager", image: null },
      expires: "2026-12-31T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "mgr_1",
      systemRole: SystemRole.MANAGER,
      permissionOverrides: null,
    },
  });
}

function createAdminCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "admin@example.com", name: "Admin", image: null },
      expires: "2026-12-31T00:00:00.000Z",
    },
    db: db as never,
    dbUser: {
      id: "admin_1",
      systemRole: SystemRole.ADMIN,
      permissionOverrides: null,
    },
  });
}

function createUnauthenticatedCaller(db: Record<string, unknown>) {
  return createCaller({
    session: null,
    db: db as never,
    dbUser: null,
  });
}

const sampleVacation = {
  id: "vac_1",
  resourceId: "res_1",
  type: VacationType.ANNUAL,
  status: VacationStatus.PENDING,
  startDate: new Date("2026-06-01"),
  endDate: new Date("2026-06-05"),
  note: "Summer vacation",
  isHalfDay: false,
  halfDayPart: null,
  deductedDays: null,
  holidayCountryCode: null,
  holidayCountryName: null,
  holidayFederalState: null,
  holidayMetroCityName: null,
  holidayCalendarDates: null,
  holidayLegacyPublicHolidayDates: null,
  requestedById: "user_1",
  approvedById: null,
  approvedAt: null,
  rejectionReason: null,
  createdAt: new Date("2026-03-01"),
  updatedAt: new Date("2026-03-01"),
  resource: { id: "res_1", displayName: "Alice", eid: "E-001" },
  requestedBy: { id: "user_1", name: "User", email: "user@example.com" },
  approvedBy: null,
};

function createVacationDb(overrides: Record<string, unknown> = {}) {
  const db = {
    user: {
      findUnique: vi.fn().mockResolvedValue({ id: "user_1", systemRole: "USER" }),
      findMany: vi.fn().mockResolvedValue([{ id: "mgr_1" }, { id: "admin_1" }]),
    },
    resource: {
      findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
      findUnique: vi.fn().mockImplementation(async (args?: { select?: Record<string, unknown> }) => {
        const select = args?.select ?? {};
        return {
          ...(select.userId ? { userId: "user_1" } : {}),
          ...(select.displayName ? { displayName: "Alice" } : {}),
          ...(select.user ? { user: null } : {}),
          ...(select.federalState ? { federalState: "BY" } : {}),
          ...(select.country ? { country: { code: "DE", name: "Germany" } } : {}),
          ...(select.metroCity ? { metroCity: null } : {}),
        };
      }),
      count: vi.fn().mockResolvedValue(0),
    },
    vacation: {
      findFirst: vi.fn().mockResolvedValue(null),
      findUnique: vi.fn().mockResolvedValue(sampleVacation),
      findMany: vi.fn().mockResolvedValue([]),
      create: vi.fn().mockResolvedValue(sampleVacation),
      update: vi.fn().mockResolvedValue(sampleVacation),
      updateMany: vi.fn().mockResolvedValue({ count: 1 }),
    },
    notification: {
      updateMany: vi.fn().mockResolvedValue({ count: 1 }),
    },
    auditLog: {
      create: vi.fn().mockResolvedValue({}),
    },
  };

  return {
    ...db,
    ...overrides,
    user: { ...db.user, ...(overrides.user as Record<string, unknown> | undefined) },
    resource: { ...db.resource, ...(overrides.resource as Record<string, unknown> | undefined) },
    vacation: { ...db.vacation, ...(overrides.vacation as Record<string, unknown> | undefined) },
    notification: {
      ...db.notification,
      ...(overrides.notification as Record<string, unknown> | undefined),
    },
    auditLog: { ...db.auditLog, ...(overrides.auditLog as Record<string, unknown> | undefined) },
  };
}

describe("vacation router", () => {
  describe("list", () => {
    it("returns vacations with default filters", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([sampleVacation]),
        },
      };

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

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe("vac_1");
      expect(db.vacation.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          orderBy: { startDate: "asc" },
          take: 100,
        }),
      );
    });

    it("applies resourceId filter", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([]),
        },
      };

      const caller = createProtectedCaller(db);
      await caller.list({ resourceId: "res_1" });

      expect(db.vacation.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ resourceId: "res_1" }),
        }),
      );
    });

    it("scopes regular users to their own resource when no filter is provided", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([]),
        },
      };

      const caller = createProtectedCaller(db);
      await caller.list({});

      expect(db.vacation.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({ resourceId: "res_own" }),
        }),
      );
    });

    it("forbids regular users from listing another resource's vacations", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
        },
        vacation: {
          findMany: vi.fn(),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(caller.list({ resourceId: "res_other" })).rejects.toThrow(
        "You can only view vacation data for your own resource",
      );
      expect(db.vacation.findMany).not.toHaveBeenCalled();
    });

    it("applies status and type filters", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([]),
        },
      };

      const caller = createProtectedCaller(db);
      await caller.list({
        status: VacationStatus.APPROVED,
        type: VacationType.SICK,
      });

      expect(db.vacation.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            status: VacationStatus.APPROVED,
            type: VacationType.SICK,
          }),
        }),
      );
    });

    it("rejects unauthenticated users", async () => {
      const db = { vacation: { findMany: vi.fn() } };
      const caller = createUnauthenticatedCaller(db);

      await expect(caller.list({})).rejects.toThrow("Authentication required");
    });
  });

  describe("background side effects", () => {
    it("logs and swallows async notification failures during approval", async () => {
      vi.mocked(createNotification).mockRejectedValueOnce(new Error("notification down"));

      const db = createVacationDb({
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }),
        },
        resource: {
          findUnique: vi.fn().mockImplementation(async (args?: { select?: Record<string, unknown> }) => {
            const select = args?.select ?? {};
            return {
              ...(select.displayName ? { displayName: "Alice" } : {}),
              ...(select.user
                ? { user: { id: "user_1", email: "user@example.com", name: "User" } }
                : {}),
            };
          }),
        },
        vacation: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleVacation,
            status: VacationStatus.PENDING,
          }),
          update: vi.fn().mockResolvedValue({
            ...sampleVacation,
            status: VacationStatus.APPROVED,
          }),
        },
      });

      const caller = createManagerCaller(db);
      const result = await caller.approve({ id: "vac_1" });

      await Promise.resolve();
      await Promise.resolve();

      expect(result.status).toBe(VacationStatus.APPROVED);
      expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
        expect.objectContaining({
          effectName: "notifyVacationStatus",
          vacationId: "vac_1",
          resourceId: "res_1",
          newStatus: VacationStatus.APPROVED,
        }),
        "Vacation background side effect failed",
      );
    });

    it("logs and swallows webhook failures during approval", async () => {
      vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook down"));

      const db = createVacationDb({
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }),
        },
        resource: {
          findUnique: vi.fn().mockImplementation(async (args?: { select?: Record<string, unknown> }) => {
            const select = args?.select ?? {};
            return {
              ...(select.displayName ? { displayName: "Alice" } : {}),
              ...(select.user
                ? { user: { id: "user_1", email: "user@example.com", name: "User" } }
                : {}),
            };
          }),
        },
        vacation: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleVacation,
            status: VacationStatus.PENDING,
          }),
          update: vi.fn().mockResolvedValue({
            ...sampleVacation,
            status: VacationStatus.APPROVED,
          }),
        },
      });

      const caller = createManagerCaller(db);
      const result = await caller.approve({ id: "vac_1" });

      await Promise.resolve();
      await Promise.resolve();

      expect(result.status).toBe(VacationStatus.APPROVED);
      expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
        expect.objectContaining({ effectName: "dispatchWebhooks", event: "vacation.approved" }),
        "Vacation background side effect failed",
      );
    });
  });

  describe("getById", () => {
    it("returns vacation by id", async () => {
      const db = {
        resource: {
          findFirst: vi.fn(),
        },
        vacation: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleVacation,
            resource: { ...sampleVacation.resource, userId: "user_1" },
          }),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.getById({ id: "vac_1" });

      expect(result.id).toBe("vac_1");
      expect(db.vacation.findUnique).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "vac_1" },
        }),
      );
    });

    it("forbids regular users from reading another user's vacation", async () => {
      const db = {
        vacation: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleVacation,
            requestedById: "someone_else",
            resource: { ...sampleVacation.resource, userId: "someone_else" },
          }),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(caller.getById({ id: "vac_1" })).rejects.toThrow(
        "You can only view your own vacation data",
      );
    });

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

      const caller = createProtectedCaller(db);
      await expect(caller.getById({ id: "missing" })).rejects.toThrow("Vacation not found");
    });
  });

  describe("create", () => {
    it("creates vacation as PENDING for regular user", async () => {
      const createdVacation = {
        ...sampleVacation,
        status: VacationStatus.PENDING,
      };

      const db = createVacationDb({
        vacation: {
          create: vi.fn().mockResolvedValue(createdVacation),
        },
      });

      const caller = createProtectedCaller(db);
      const result = await caller.create({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-05"),
      });

      expect(result.status).toBe(VacationStatus.PENDING);
      expect(db.vacation.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            status: VacationStatus.PENDING,
            resourceId: "res_1",
            type: VacationType.ANNUAL,
          }),
        }),
      );
    });

    it("creates vacation as APPROVED for manager", async () => {
      const createdVacation = {
        ...sampleVacation,
        status: VacationStatus.APPROVED,
        approvedById: "mgr_1",
      };

      const db = createVacationDb({
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }),
        },
        vacation: {
          create: vi.fn().mockResolvedValue(createdVacation),
        },
      });

      const caller = createManagerCaller(db);
      const result = await caller.create({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-05"),
      });

      expect(result.status).toBe(VacationStatus.APPROVED);
      expect(db.vacation.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            status: VacationStatus.APPROVED,
            approvedById: "mgr_1",
          }),
        }),
      );
    });

    it("rejects overlapping vacation", async () => {
      const db = createVacationDb({
        vacation: {
          findFirst: vi.fn().mockResolvedValue({ id: "existing_vac" }),
        },
      });

      const caller = createProtectedCaller(db);
      await expect(
        caller.create({
          resourceId: "res_1",
          type: VacationType.ANNUAL,
          startDate: new Date("2026-06-01"),
          endDate: new Date("2026-06-05"),
        }),
      ).rejects.toThrow("Overlapping vacation already exists");
    });

    it("rejects when end date is before start date", async () => {
      const db = createVacationDb({
        user: { findUnique: vi.fn() },
        vacation: { findFirst: vi.fn() },
      });

      const caller = createProtectedCaller(db);
      await expect(
        caller.create({
          resourceId: "res_1",
          type: VacationType.ANNUAL,
          startDate: new Date("2026-06-05"),
          endDate: new Date("2026-06-01"),
        }),
      ).rejects.toThrow();
    });

    it("supports half-day vacations", async () => {
      const createdVacation = {
        ...sampleVacation,
        isHalfDay: true,
        halfDayPart: "MORNING",
      };

      const db = createVacationDb({
        vacation: {
          create: vi.fn().mockResolvedValue(createdVacation),
        },
      });

      const caller = createProtectedCaller(db);
      const result = await caller.create({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-01"),
        isHalfDay: true,
        halfDayPart: "MORNING",
      });

      expect(result.isHalfDay).toBe(true);
      expect(db.vacation.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            isHalfDay: true,
            halfDayPart: "MORNING",
          }),
        }),
      );
    });

    it("rejects multi-day half-day vacations", async () => {
      const db = createVacationDb();
      const caller = createProtectedCaller(db);

      await expect(caller.create({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-02"),
        isHalfDay: true,
        halfDayPart: "MORNING",
      })).rejects.toThrow();

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

    it("rejects half-day vacations without a half-day part", async () => {
      const db = createVacationDb();
      const caller = createProtectedCaller(db);

      await expect(caller.create({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-01"),
        isHalfDay: true,
      })).rejects.toThrow();

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

    it("rejects half-day parts on full-day vacations", async () => {
      const db = createVacationDb();
      const caller = createProtectedCaller(db);

      await expect(caller.create({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-01"),
        halfDayPart: "AFTERNOON",
      })).rejects.toThrow();

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

    it("rejects leave requests that only hit public holidays", async () => {
      const db = createVacationDb({
        vacation: {
          findMany: vi.fn().mockResolvedValue([]),
        },
      });

      const caller = createProtectedCaller(db);

      await expect(caller.create({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-01-06T00:00:00.000Z"),
        endDate: new Date("2026-01-06T00:00:00.000Z"),
      })).rejects.toThrow("does not deduct any vacation days");

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

    it("keeps mixed vacation ranges chargeable when only some days are holidays", async () => {
      const createdVacation = {
        ...sampleVacation,
        startDate: new Date("2026-11-14T00:00:00.000Z"),
        endDate: new Date("2026-11-16T00:00:00.000Z"),
      };
      const db = createVacationDb({
        resource: {
          findUnique: vi.fn().mockResolvedValue({
            userId: "user_1",
            countryId: "country_de",
            metroCityId: "city_muc",
            federalState: "BY",
            country: { code: "DE", name: "Germany" },
            metroCity: { name: "Muenchen" },
          }),
        },
        holidayCalendar: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "cal_muc",
              name: "Muenchen lokal",
              scopeType: "CITY",
              priority: 10,
              createdAt: new Date("2026-01-01T00:00:00.000Z"),
              entries: [
                {
                  date: new Date("2020-11-15T00:00:00.000Z"),
                  name: "Lokaler Stadtfeiertag",
                  isRecurringAnnual: true,
                },
              ],
            },
          ]),
        },
        vacation: {
          findFirst: vi.fn().mockResolvedValue(null),
          findMany: vi.fn().mockResolvedValue([]),
          create: vi.fn().mockResolvedValue(createdVacation),
        },
      });

      const caller = createProtectedCaller(db);
      const result = await caller.create({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-11-14T00:00:00.000Z"),
        endDate: new Date("2026-11-16T00:00:00.000Z"),
      });

      expect(result.effectiveDays).toBe(2);
      expect(db.vacation.create).toHaveBeenCalledWith(expect.objectContaining({
        data: expect.objectContaining({
          deductedDays: 2,
          holidayCountryCode: "DE",
          holidayCountryName: "Germany",
          holidayFederalState: "BY",
          holidayMetroCityName: "Muenchen",
          holidayCalendarDates: ["2026-11-15"],
          holidayLegacyPublicHolidayDates: [],
        }),
      }));
    });
  });

  describe("previewRequest", () => {
    it("shows public holidays as non-deductible leave days", async () => {
      const db = createVacationDb({
        resource: {
          findUnique: vi.fn().mockResolvedValue({
            userId: "user_1",
            federalState: "BY",
            country: { code: "DE", name: "Germany" },
            metroCity: { name: "Augsburg" },
          }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([]),
        },
      });

      const caller = createProtectedCaller(db);
      const result = await caller.previewRequest({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2028-08-08T00:00:00.000Z"),
        endDate: new Date("2028-08-08T00:00:00.000Z"),
      });

      expect(result.requestedDays).toBe(1);
      expect(result.effectiveDays).toBe(0);
      expect(result.deductedDays).toBe(0);
      expect(result.publicHolidayDates).toContain("2028-08-08");
      expect(result.holidayContext).toEqual({
        countryCode: "DE",
        countryName: "Germany",
        federalState: "BY",
        metroCityName: "Augsburg",
        sources: {
          hasCalendarHolidays: true,
          hasLegacyPublicHolidayEntries: false,
        },
      });
      expect(result.holidayDetails).toContainEqual({
        date: "2028-08-08",
        source: "CALENDAR",
      });
    });

    it("uses custom city holiday calendars for non-deductible leave days", async () => {
      const db = createVacationDb({
        resource: {
          findUnique: vi.fn().mockResolvedValue({
            userId: "user_1",
            countryId: "country_de",
            metroCityId: "city_muc",
            federalState: "BY",
            country: { code: "DE", name: "Germany" },
            metroCity: { name: "Muenchen" },
          }),
        },
        holidayCalendar: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "cal_muc",
              name: "Muenchen lokal",
              scopeType: "CITY",
              priority: 10,
              createdAt: new Date("2026-01-01T00:00:00.000Z"),
              entries: [
                {
                  date: new Date("2020-11-15T00:00:00.000Z"),
                  name: "Lokaler Stadtfeiertag",
                  isRecurringAnnual: true,
                },
              ],
            },
          ]),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([]),
        },
      });

      const caller = createProtectedCaller(db);
      const result = await caller.previewRequest({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-11-15T00:00:00.000Z"),
        endDate: new Date("2026-11-15T00:00:00.000Z"),
      });

      expect(result.requestedDays).toBe(1);
      expect(result.effectiveDays).toBe(0);
      expect(result.publicHolidayDates).toContain("2026-11-15");
      expect(result.holidayContext.countryName).toBe("Germany");
      expect(result.holidayContext.metroCityName).toBe("Muenchen");
      expect(db.holidayCalendar.findMany).toHaveBeenCalled();
    });

    it("marks legacy public holiday entries as a separate preview source", async () => {
      const db = createVacationDb({
        resource: {
          findUnique: vi.fn().mockResolvedValue({
            userId: "user_1",
            federalState: "HH",
            country: { code: "DE", name: "Germany" },
            metroCity: { name: "Hamburg" },
          }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([
            {
              startDate: new Date("2026-05-01T00:00:00.000Z"),
              endDate: new Date("2026-05-01T00:00:00.000Z"),
            },
          ]),
        },
      });

      const caller = createProtectedCaller(db);
      const result = await caller.previewRequest({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-05-01T00:00:00.000Z"),
        endDate: new Date("2026-05-01T00:00:00.000Z"),
      });

      expect(result.publicHolidayDates).toContain("2026-05-01");
      expect(result.holidayContext.sources).toEqual({
        hasCalendarHolidays: true,
        hasLegacyPublicHolidayEntries: true,
      });
      expect(result.holidayDetails).toContainEqual({
        date: "2026-05-01",
        source: "CALENDAR_AND_LEGACY",
      });
    });

    it("rejects multi-day half-day previews", async () => {
      const db = createVacationDb();
      const caller = createProtectedCaller(db);

      await expect(caller.previewRequest({
        resourceId: "res_1",
        type: VacationType.ANNUAL,
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-02"),
        isHalfDay: true,
      })).rejects.toThrow();
    });
  });

  describe("create manual public holiday handling", () => {
    it("rejects manual public holiday creation requests", async () => {
      const db = createVacationDb();
      const caller = createManagerCaller(db);

      await expect(caller.create({
        resourceId: "res_1",
        type: VacationType.PUBLIC_HOLIDAY,
        startDate: new Date("2026-05-01T00:00:00.000Z"),
        endDate: new Date("2026-05-01T00:00:00.000Z"),
      })).rejects.toThrow("Public holidays must be managed via Holiday Calendars or the legacy holiday import");

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

  describe("approve", () => {
    it("approves a PENDING vacation", async () => {
      const updatedVacation = {
        ...sampleVacation,
        status: VacationStatus.APPROVED,
        approvedById: "mgr_1",
      };

      const db = createVacationDb({
        vacation: {
          findUnique: vi.fn().mockResolvedValue(sampleVacation),
          update: vi.fn().mockResolvedValue(updatedVacation),
        },
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }),
        },
        resource: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      });

      const caller = createManagerCaller(db);
      const result = await caller.approve({ id: "vac_1" });

      expect(result.status).toBe(VacationStatus.APPROVED);
      expect(db.vacation.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "vac_1" },
          data: expect.objectContaining({
            status: VacationStatus.APPROVED,
            rejectionReason: null,
            deductedDays: 5,
            holidayCalendarDates: [],
            holidayLegacyPublicHolidayDates: [],
          }),
        }),
      );
    });

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

      const caller = createManagerCaller(db);
      await expect(caller.approve({ id: "missing" })).rejects.toThrow("Vacation not found");
    });

    it("rejects approving an already APPROVED vacation", async () => {
      const db = createVacationDb({
        vacation: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleVacation,
            status: VacationStatus.APPROVED,
          }),
        },
      });

      const caller = createManagerCaller(db);
      await expect(caller.approve({ id: "vac_1" })).rejects.toThrow(
        "Only PENDING, CANCELLED, or REJECTED vacations can be approved",
      );
    });

    it("rejects approval when the current holiday context reduces the request to zero days", async () => {
      const db = createVacationDb({
        resource: {
          findUnique: vi.fn().mockResolvedValue({
            countryId: "country_de",
            metroCityId: "city_muc",
            federalState: "BY",
            country: { code: "DE", name: "Germany" },
            metroCity: { name: "Muenchen" },
          }),
        },
        holidayCalendar: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "cal_muc",
              name: "Muenchen lokal",
              scopeType: "CITY",
              priority: 10,
              createdAt: new Date("2026-01-01T00:00:00.000Z"),
              entries: [
                {
                  date: new Date("2020-11-15T00:00:00.000Z"),
                  name: "Lokaler Stadtfeiertag",
                  isRecurringAnnual: true,
                },
              ],
            },
          ]),
        },
        vacation: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleVacation,
            startDate: new Date("2026-11-15T00:00:00.000Z"),
            endDate: new Date("2026-11-15T00:00:00.000Z"),
          }),
          update: vi.fn(),
          findMany: vi.fn().mockResolvedValue([]),
        },
      });

      const caller = createManagerCaller(db);
      await expect(caller.approve({ id: "vac_1" })).rejects.toThrow(
        "Vacation no longer deducts any vacation days for the current holiday calendar and cannot be approved",
      );
      expect(db.vacation.update).not.toHaveBeenCalled();
    });

    it("forbids regular users from approving", async () => {
      const db = {};
      const caller = createProtectedCaller(db);
      await expect(caller.approve({ id: "vac_1" })).rejects.toThrow();
    });
  });

  describe("reject", () => {
    it("rejects a PENDING vacation", async () => {
      const updatedVacation = {
        ...sampleVacation,
        status: VacationStatus.REJECTED,
        rejectionReason: "Team conflict",
      };

      const db = createVacationDb({
        vacation: {
          findUnique: vi.fn().mockResolvedValue(sampleVacation),
          update: vi.fn().mockResolvedValue(updatedVacation),
        },
        resource: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      });

      const caller = createManagerCaller(db);
      const result = await caller.reject({ id: "vac_1", rejectionReason: "Team conflict" });

      expect(result.status).toBe(VacationStatus.REJECTED);
      expect(db.vacation.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            status: VacationStatus.REJECTED,
            rejectionReason: "Team conflict",
          }),
        }),
      );
    });

    it("throws when rejecting non-PENDING vacation", async () => {
      const db = createVacationDb({
        vacation: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleVacation,
            status: VacationStatus.APPROVED,
          }),
        },
      });

      const caller = createManagerCaller(db);
      await expect(caller.reject({ id: "vac_1" })).rejects.toThrow(
        "Only PENDING vacations can be rejected",
      );
    });
  });

  describe("cancel", () => {
    it("cancels an existing vacation", async () => {
      const updatedVacation = {
        ...sampleVacation,
        status: VacationStatus.CANCELLED,
      };

      const db = createVacationDb({
        vacation: {
          findUnique: vi.fn().mockResolvedValue(sampleVacation),
          update: vi.fn().mockResolvedValue(updatedVacation),
        },
      });

      const caller = createProtectedCaller(db);
      const result = await caller.cancel({ id: "vac_1" });

      expect(result.status).toBe(VacationStatus.CANCELLED);
    });

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

      const caller = createProtectedCaller(db);
      await expect(caller.cancel({ id: "missing" })).rejects.toThrow("Vacation not found");
    });

    it("throws when already cancelled", async () => {
      const db = createVacationDb({
        vacation: {
          findUnique: vi.fn().mockResolvedValue({
            ...sampleVacation,
            status: VacationStatus.CANCELLED,
          }),
        },
      });

      const caller = createProtectedCaller(db);
      await expect(caller.cancel({ id: "vac_1" })).rejects.toThrow("Already cancelled");
    });
  });

  describe("batchApprove", () => {
    it("approves multiple pending vacations", async () => {
      const db = createVacationDb({
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }),
        },
        vacation: {
          findMany: vi.fn().mockImplementation(async ({ where }: { where?: { status?: string; type?: string } } = {}) => {
            if (where?.type === VacationType.PUBLIC_HOLIDAY) {
              return [];
            }
            if (where?.status === VacationStatus.PENDING) {
              return [
                {
                  id: "vac_1",
                  resourceId: "res_1",
                  type: VacationType.ANNUAL,
                  startDate: new Date("2026-06-01T00:00:00.000Z"),
                  endDate: new Date("2026-06-05T00:00:00.000Z"),
                  isHalfDay: false,
                },
                {
                  id: "vac_2",
                  resourceId: "res_2",
                  type: VacationType.OTHER,
                  startDate: new Date("2026-06-08T00:00:00.000Z"),
                  endDate: new Date("2026-06-08T00:00:00.000Z"),
                  isHalfDay: false,
                },
              ];
            }
            return [];
          }),
          update: vi.fn().mockResolvedValue(sampleVacation),
        },
        resource: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      });

      const caller = createManagerCaller(db);
      const result = await caller.batchApprove({ ids: ["vac_1", "vac_2"] });

      expect(result.approved).toBe(2);
      expect(db.vacation.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "vac_1" },
          data: expect.objectContaining({
            status: VacationStatus.APPROVED,
            deductedDays: 5,
          }),
        }),
      );
    });

    it("only approves PENDING vacations from the requested set", async () => {
      const db = createVacationDb({
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }),
        },
        vacation: {
          findMany: vi.fn().mockImplementation(async ({ where }: { where?: { status?: string; type?: string } } = {}) => {
            if (where?.type === VacationType.PUBLIC_HOLIDAY) {
              return [];
            }
            if (where?.status === VacationStatus.PENDING) {
              return [
                {
                  id: "vac_1",
                  resourceId: "res_1",
                  type: VacationType.ANNUAL,
                  startDate: new Date("2026-06-01T00:00:00.000Z"),
                  endDate: new Date("2026-06-01T00:00:00.000Z"),
                  isHalfDay: false,
                },
              ];
            }
            return [];
          }),
          update: vi.fn().mockResolvedValue(sampleVacation),
        },
        resource: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      });

      const caller = createManagerCaller(db);
      const result = await caller.batchApprove({ ids: ["vac_1", "vac_already_approved"] });

      expect(result.approved).toBe(1);
      expect(db.vacation.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: { in: ["vac_1", "vac_already_approved"] }, status: VacationStatus.PENDING },
        }),
      );
    });
  });

  describe("batchReject", () => {
    it("rejects multiple pending vacations with optional reason", async () => {
      const db = createVacationDb({
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([
            { id: "vac_1", resourceId: "res_1" },
          ]),
          updateMany: vi.fn().mockResolvedValue({ count: 1 }),
        },
        resource: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
      });

      const caller = createManagerCaller(db);
      const result = await caller.batchReject({
        ids: ["vac_1"],
        rejectionReason: "Budget freeze",
      });

      expect(result.rejected).toBe(1);
      expect(db.vacation.updateMany).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            status: VacationStatus.REJECTED,
            rejectionReason: "Budget freeze",
          }),
        }),
      );
    });
  });

  describe("getForResource", () => {
    it("returns approved vacations in date range", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "vac_1",
              startDate: new Date("2026-06-01"),
              endDate: new Date("2026-06-05"),
              type: VacationType.ANNUAL,
              status: VacationStatus.APPROVED,
            },
          ]),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.getForResource({
        resourceId: "res_1",
        startDate: new Date("2026-01-01"),
        endDate: new Date("2026-12-31"),
      });

      expect(result).toHaveLength(1);
      expect(db.vacation.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            resourceId: "res_1",
            status: VacationStatus.APPROVED,
          }),
        }),
      );
    });

    it("forbids regular users from reading another resource's approved vacations", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
        },
        vacation: {
          findMany: vi.fn(),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.getForResource({
          resourceId: "res_other",
          startDate: new Date("2026-01-01"),
          endDate: new Date("2026-12-31"),
        }),
      ).rejects.toThrow("You can only view vacation data for your own resource");
      expect(db.vacation.findMany).not.toHaveBeenCalled();
    });
  });

  describe("getPendingApprovals", () => {
    it("returns all pending vacations for managers", async () => {
      const db = {
        vacation: {
          findMany: vi.fn().mockResolvedValue([sampleVacation]),
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.getPendingApprovals();

      expect(result).toHaveLength(1);
      expect(db.vacation.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { status: VacationStatus.PENDING },
        }),
      );
    });

    it("forbids regular users from viewing pending approvals", async () => {
      const db = {};
      const caller = createProtectedCaller(db);
      await expect(caller.getPendingApprovals()).rejects.toThrow();
    });
  });

  describe("getTeamOverlap", () => {
    it("returns overlapping vacations for the same chapter", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
          findUnique: vi.fn().mockResolvedValue({ chapter: "Animation" }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([
            {
              ...sampleVacation,
              id: "vac_other",
              resourceId: "res_2",
              resource: { id: "res_2", displayName: "Bob", eid: "E-002" },
            },
          ]),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.getTeamOverlap({
        resourceId: "res_1",
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-05"),
      });

      expect(result).toHaveLength(1);
      expect(db.vacation.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            resource: { chapter: "Animation" },
            resourceId: { not: "res_1" },
          }),
        }),
      );
    });

    it("returns empty array when resource has no chapter", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
          findUnique: vi.fn().mockResolvedValue({ chapter: null }),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.getTeamOverlap({
        resourceId: "res_1",
        startDate: new Date("2026-06-01"),
        endDate: new Date("2026-06-05"),
      });

      expect(result).toEqual([]);
    });

    it("forbids regular users from reading another resource's team overlap", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
          findUnique: vi.fn(),
        },
        vacation: {
          findMany: vi.fn(),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.getTeamOverlap({
          resourceId: "res_other",
          startDate: new Date("2026-06-01"),
          endDate: new Date("2026-06-05"),
        }),
      ).rejects.toThrow("You can only view vacation data for your own resource");
      expect(db.resource.findUnique).not.toHaveBeenCalled();
      expect(db.vacation.findMany).not.toHaveBeenCalled();
    });
  });

  describe("getTeamOverlapDetail", () => {
    it("returns assistant-friendly overlap detail from the canonical overlap query", async () => {
      const db = {
        resource: {
          findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
          findUnique: vi
            .fn()
            .mockResolvedValueOnce({ displayName: "Bruce Banner", chapter: "CGI" }),
        },
        vacation: {
          findMany: vi.fn().mockResolvedValue([
            {
              ...sampleVacation,
              id: "vac_other",
              resourceId: "res_2",
              status: VacationStatus.APPROVED,
              resource: { id: "res_2", displayName: "Clark Kent", eid: "E-002" },
            },
          ]),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.getTeamOverlapDetail({
        resourceId: "res_1",
        startDate: new Date("2026-08-10T00:00:00.000Z"),
        endDate: new Date("2026-08-12T00:00:00.000Z"),
      });

      expect(result).toEqual({
        resource: "Bruce Banner",
        chapter: "CGI",
        period: "2026-08-10 to 2026-08-12",
        overlapCount: 1,
        overlappingVacations: [
          {
            resource: "Clark Kent",
            type: VacationType.ANNUAL,
            status: VacationStatus.APPROVED,
            start: "2026-06-01",
            end: "2026-06-05",
          },
        ],
      });
    });
  });

  describe("batchCreatePublicHolidays", () => {
    it("creates public holidays for all active resources (admin only)", async () => {
      const db = {
        resource: {
          findMany: vi.fn().mockResolvedValue([
            { id: "res_1", federalState: "BY", country: { code: "DE" }, metroCity: null },
            { id: "res_2", federalState: "BY", country: { code: "DE" }, metroCity: null },
          ]),
        },
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "admin_1" }),
        },
        vacation: {
          deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
          findFirst: vi.fn().mockResolvedValue(null),
          create: vi.fn().mockResolvedValue({}),
        },
      };

      const caller = createAdminCaller(db);
      const result = await caller.batchCreatePublicHolidays({
        year: 2026,
        federalState: "BY",
      });

      expect(result.created).toBeGreaterThan(0);
      expect(result.resources).toBe(2);
      expect(db.vacation.create).toHaveBeenCalled();
    });

    it("skips already existing holidays", async () => {
      const db = {
        resource: {
          findMany: vi.fn().mockResolvedValue([
            { id: "res_1", federalState: "BY", country: { code: "DE" }, metroCity: null },
          ]),
        },
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "admin_1" }),
        },
        vacation: {
          findFirst: vi.fn().mockResolvedValue({ id: "existing" }),
          create: vi.fn(),
        },
      };

      const caller = createAdminCaller(db);
      const result = await caller.batchCreatePublicHolidays({
        year: 2026,
        federalState: "BY",
      });

      expect(result.created).toBe(0);
      expect(db.vacation.create).not.toHaveBeenCalled();
    });

    it("forbids non-admin users", async () => {
      const db = {};
      const caller = createManagerCaller(db);
      await expect(
        caller.batchCreatePublicHolidays({ year: 2026 }),
      ).rejects.toThrow("Admin role required");
    });
  });

  describe("updateStatus", () => {
    it("allows manager to approve via updateStatus", async () => {
      const updatedVacation = {
        ...sampleVacation,
        status: VacationStatus.APPROVED,
      };

      const db = {
        vacation: {
          findUnique: vi.fn().mockResolvedValue(sampleVacation),
          update: vi.fn().mockResolvedValue(updatedVacation),
        },
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }),
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.updateStatus({ id: "vac_1", status: "APPROVED" });

      expect(result.status).toBe(VacationStatus.APPROVED);
    });

    it("allows any user to cancel via updateStatus", async () => {
      const updatedVacation = {
        ...sampleVacation,
        status: VacationStatus.CANCELLED,
      };

      const db = {
        vacation: {
          findUnique: vi.fn().mockResolvedValue(sampleVacation),
          update: vi.fn().mockResolvedValue(updatedVacation),
        },
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "user_1", systemRole: "USER" }),
        },
      };

      const caller = createProtectedCaller(db);
      const result = await caller.updateStatus({ id: "vac_1", status: "CANCELLED" });

      expect(result.status).toBe(VacationStatus.CANCELLED);
    });

    it("forbids non-manager from approving via updateStatus", async () => {
      const db = {
        vacation: {
          findUnique: vi.fn().mockResolvedValue(sampleVacation),
        },
        user: {
          findUnique: vi.fn().mockResolvedValue({ id: "user_1", systemRole: "USER" }),
        },
      };

      const caller = createProtectedCaller(db);
      await expect(
        caller.updateStatus({ id: "vac_1", status: "APPROVED" }),
      ).rejects.toThrow("Manager role required to approve/reject");
    });

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

      const caller = createProtectedCaller(db);
      await expect(
        caller.updateStatus({ id: "missing", status: "CANCELLED" }),
      ).rejects.toThrow("Vacation not found");
    });
  });
});
