import { TRPCError } from "@trpc/server";
import { describe, expect, it, vi } from "vitest";
import { setEntitlement, bulkSetEntitlements } from "../use-cases/entitlement/set-entitlement.js";
import { syncEntitlement } from "../use-cases/entitlement/sync-entitlement.js";
import { getEntitlementBalance } from "../use-cases/entitlement/read-entitlement-balance.js";
import type { SyncEntitlementDeps } from "../use-cases/entitlement/sync-entitlement.js";
import type { ReadEntitlementBalanceDeps } from "../use-cases/entitlement/read-entitlement-balance.js";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeEntitlement(overrides: Record<string, unknown> = {}) {
  return {
    id: "ent_1",
    resourceId: "resource_1",
    year: 2026,
    entitledDays: 28,
    carryoverDays: 0,
    usedDays: 0,
    pendingDays: 0,
    createdAt: new Date("2026-01-01"),
    updatedAt: new Date("2026-01-01"),
    ...overrides,
  };
}

function makeSyncDeps(overrides: Partial<SyncEntitlementDeps> = {}): SyncEntitlementDeps {
  return {
    loadResourceHolidayContext: vi.fn().mockResolvedValue({
      countryCode: "DE",
      countryName: "Germany",
      federalState: null,
      metroCityName: null,
      calendarHolidayStrings: [],
      publicHolidayStrings: [],
    }),
    countCalendarDaysInPeriod: vi.fn().mockReturnValue(0),
    countVacationChargeableDays: vi.fn().mockReturnValue(0),
    countVacationChargeableDaysFromSnapshot: vi.fn().mockReturnValue(null),
    ...overrides,
  };
}

function makeReadDeps(
  overrides: Partial<ReadEntitlementBalanceDeps> = {},
): ReadEntitlementBalanceDeps {
  return {
    ...makeSyncDeps(),
    buildVacationPreview: vi.fn().mockReturnValue({
      requestedDays: 0,
      deductedDays: 0,
      holidayDetails: [],
      holidayContext: {
        countryCode: null,
        countryName: null,
        federalState: null,
        metroCityName: null,
        sources: { hasCalendarHolidays: false, hasLegacyPublicHolidayEntries: false },
      },
    }),
    parseVacationSnapshotDateList: vi.fn().mockReturnValue([]),
    ...overrides,
  };
}

// ---------------------------------------------------------------------------
// setEntitlement
// ---------------------------------------------------------------------------

describe("setEntitlement", () => {
  it("creates a new entitlement when none exists for resource+year", async () => {
    const created = makeEntitlement();
    const db = {
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValue(null),
        create: vi.fn().mockResolvedValue(created),
        update: vi.fn(),
        upsert: vi.fn(),
      },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    const result = await setEntitlement(db as never, {
      resourceId: "resource_1",
      year: 2026,
      entitledDays: 28,
    });

    expect(result.existing).toBeNull();
    expect(result.result.id).toBe("ent_1");
    expect(db.vacationEntitlement.create).toHaveBeenCalledOnce();
    expect(db.vacationEntitlement.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          resourceId: "resource_1",
          year: 2026,
          entitledDays: 28,
          carryoverDays: 0,
          usedDays: 0,
          pendingDays: 0,
        }),
      }),
    );
    expect(db.vacationEntitlement.update).not.toHaveBeenCalled();
  });

  it("updates an existing entitlement and returns the previous snapshot as existing", async () => {
    const existing = makeEntitlement({ entitledDays: 20 });
    const updated = makeEntitlement({ entitledDays: 30 });
    const db = {
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValue(existing),
        update: vi.fn().mockResolvedValue(updated),
        create: vi.fn(),
        upsert: vi.fn(),
      },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    const result = await setEntitlement(db as never, {
      resourceId: "resource_1",
      year: 2026,
      entitledDays: 30,
    });

    expect(result.existing).toEqual(existing);
    expect((result.result as typeof updated).entitledDays).toBe(30);
    expect(db.vacationEntitlement.update).toHaveBeenCalledOnce();
    expect(db.vacationEntitlement.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "ent_1" },
        data: { entitledDays: 30 },
      }),
    );
    expect(db.vacationEntitlement.create).not.toHaveBeenCalled();
  });

  it("looks up entitlement using composite key resourceId_year", async () => {
    const db = {
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValue(null),
        create: vi.fn().mockResolvedValue(makeEntitlement()),
        update: vi.fn(),
        upsert: vi.fn(),
      },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    await setEntitlement(db as never, { resourceId: "resource_42", year: 2025, entitledDays: 25 });

    expect(db.vacationEntitlement.findUnique).toHaveBeenCalledWith({
      where: { resourceId_year: { resourceId: "resource_42", year: 2025 } },
    });
  });
});

// ---------------------------------------------------------------------------
// bulkSetEntitlements
// ---------------------------------------------------------------------------

describe("bulkSetEntitlements", () => {
  it("upserts entitlement for every active resource and returns correct count", async () => {
    const resources = [{ id: "r1" }, { id: "r2" }, { id: "r3" }];
    const db = {
      resource: { findMany: vi.fn().mockResolvedValue(resources) },
      vacationEntitlement: { upsert: vi.fn().mockResolvedValue(makeEntitlement()) },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
    };

    const result = await bulkSetEntitlements(db as never, { year: 2026, entitledDays: 28 });

    expect(result.updated).toBe(3);
    expect(db.vacationEntitlement.upsert).toHaveBeenCalledTimes(3);
  });

  it("upserts only for the specified resourceIds when provided", async () => {
    const db = {
      resource: {
        findMany: vi.fn().mockResolvedValue([{ id: "r1" }, { id: "r2" }]),
      },
      vacationEntitlement: { upsert: vi.fn().mockResolvedValue(makeEntitlement()) },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
    };

    const result = await bulkSetEntitlements(db as never, {
      year: 2026,
      entitledDays: 20,
      resourceIds: ["r1", "r2"],
    });

    expect(result.updated).toBe(2);
    // resource.findMany should have received the id filter
    expect(db.resource.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({ id: { in: ["r1", "r2"] } }),
      }),
    );
  });

  it("returns 0 when no active resources exist", async () => {
    const db = {
      resource: { findMany: vi.fn().mockResolvedValue([]) },
      vacationEntitlement: { upsert: vi.fn() },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
    };

    const result = await bulkSetEntitlements(db as never, { year: 2026, entitledDays: 28 });

    expect(result.updated).toBe(0);
    expect(db.vacationEntitlement.upsert).not.toHaveBeenCalled();
  });

  it("passes correct create and update payloads to upsert", async () => {
    const db = {
      resource: { findMany: vi.fn().mockResolvedValue([{ id: "r99" }]) },
      vacationEntitlement: { upsert: vi.fn().mockResolvedValue(makeEntitlement()) },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
    };

    await bulkSetEntitlements(db as never, { year: 2025, entitledDays: 15 });

    expect(db.vacationEntitlement.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { resourceId_year: { resourceId: "r99", year: 2025 } },
        create: expect.objectContaining({
          resourceId: "r99",
          year: 2025,
          entitledDays: 15,
          carryoverDays: 0,
          usedDays: 0,
          pendingDays: 0,
        }),
        update: { entitledDays: 15 },
      }),
    );
  });

  it("queries only active resources (isActive: true)", async () => {
    const db = {
      resource: { findMany: vi.fn().mockResolvedValue([]) },
      vacationEntitlement: { upsert: vi.fn() },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
    };

    await bulkSetEntitlements(db as never, { year: 2026, entitledDays: 28 });

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

// ---------------------------------------------------------------------------
// syncEntitlement
// ---------------------------------------------------------------------------

describe("syncEntitlement", () => {
  it("creates a new entitlement when none exists for the year (no previous year)", async () => {
    const createdEnt = makeEntitlement({ usedDays: 0, pendingDays: 0 });
    const updatedEnt = makeEntitlement({ usedDays: 0, pendingDays: 0 });

    const db = {
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValue(null), // no current year, no previous year
        create: vi.fn().mockResolvedValue(createdEnt),
        update: vi.fn().mockResolvedValue(updatedEnt),
      },
      vacation: { findMany: vi.fn().mockResolvedValue([]) },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    const deps = makeSyncDeps();

    const result = await syncEntitlement(db as never, "resource_1", 2026, 28, deps);

    expect(db.vacationEntitlement.create).toHaveBeenCalledOnce();
    expect(db.vacationEntitlement.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          resourceId: "resource_1",
          year: 2026,
          entitledDays: 28,
          carryoverDays: 0,
        }),
      }),
    );
    expect(result.usedDays).toBe(0);
    expect(result.pendingDays).toBe(0);
  });

  it("carries over remaining days from previous year when creating new entitlement", async () => {
    // Previous year has 28 entitled, 20 used, 0 pending => 8 carryover
    const prevYearEnt = makeEntitlement({
      year: 2025,
      entitledDays: 28,
      usedDays: 20,
      pendingDays: 0,
      carryoverDays: 0,
    });
    const newlyCreated = makeEntitlement({
      year: 2026,
      entitledDays: 36,
      carryoverDays: 8,
      usedDays: 0,
      pendingDays: 0,
    });
    const finalUpdated = makeEntitlement({
      year: 2026,
      entitledDays: 36,
      carryoverDays: 8,
      usedDays: 0,
      pendingDays: 0,
    });

    const db = {
      vacationEntitlement: {
        // First call: lookup current year (2026) => null
        // Second call: lookup prev year (2025) for getOrCreateEntitlement => prevYearEnt
        // Third call: lookup prev year (2025) for the top-level previousYearEntitlement check => prevYearEnt
        // (syncEntitlement also recurses into prev year, which will call findUnique again)
        findUnique: vi
          .fn()
          // top-level: check year-1 (2025) for recursive sync
          .mockResolvedValueOnce(prevYearEnt)
          // recursive sync for 2025: check year-1 (2024) => null (no further recursion)
          .mockResolvedValueOnce(null)
          // getOrCreateEntitlement for 2025 current year => found
          .mockResolvedValueOnce(prevYearEnt)
          // getOrCreateEntitlement for 2025 prev year (2024) - not reached since already found
          // getOrCreateEntitlement for 2026 current year => null (need to create)
          .mockResolvedValueOnce(null)
          // getOrCreateEntitlement for 2026 prev year (2025) => prevYearEnt
          .mockResolvedValueOnce(prevYearEnt),
        create: vi.fn().mockResolvedValue(newlyCreated),
        update: vi.fn().mockResolvedValue(finalUpdated),
      },
      vacation: { findMany: vi.fn().mockResolvedValue([]) },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    const deps = makeSyncDeps();

    const result = await syncEntitlement(db as never, "resource_1", 2026, 28, deps);

    // Should have created for 2026 with carryover = 28 - 20 - 0 = 8
    const createCall = (db.vacationEntitlement.create as ReturnType<typeof vi.fn>).mock.calls.find(
      (call) => call[0]?.data?.year === 2026,
    );
    expect(createCall).toBeDefined();
    expect(createCall![0].data.carryoverDays).toBe(8);
    expect(createCall![0].data.entitledDays).toBe(36); // 28 + 8
  });

  it("throws INTERNAL_SERVER_ERROR when cycle detection triggers (visitedYears already contains year)", async () => {
    const db = {
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValue(null),
        create: vi.fn(),
        update: vi.fn(),
      },
      vacation: { findMany: vi.fn() },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    const deps = makeSyncDeps();
    const visitedWithCycle = new Set([2026]);

    await expect(
      syncEntitlement(db as never, "resource_1", 2026, 28, deps, visitedWithCycle),
    ).rejects.toThrow(TRPCError);

    await expect(
      syncEntitlement(db as never, "resource_1", 2026, 28, deps, new Set([2026])),
    ).rejects.toMatchObject({
      code: "INTERNAL_SERVER_ERROR",
      message: expect.stringContaining("2026"),
    });
  });

  it("does not call create when entitlement already exists for the year", async () => {
    const existingEnt = makeEntitlement({ entitledDays: 28, carryoverDays: 0 });
    const updatedEnt = makeEntitlement({
      entitledDays: 28,
      carryoverDays: 0,
      usedDays: 0,
      pendingDays: 0,
    });

    const db = {
      vacationEntitlement: {
        // top-level: no prev year
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(null) // year-1 check (2025) for recursive sync — no prev year
          // getOrCreateEntitlement for 2026 => found
          .mockResolvedValueOnce(existingEnt),
        create: vi.fn(),
        update: vi.fn().mockResolvedValue(updatedEnt),
      },
      vacation: { findMany: vi.fn().mockResolvedValue([]) },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    const deps = makeSyncDeps();

    await syncEntitlement(db as never, "resource_1", 2026, 28, deps);

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

  it("accumulates usedDays from APPROVED vacations and pendingDays from PENDING vacations", async () => {
    const { VacationStatus } = await import("@capakraken/db");
    const existingEnt = makeEntitlement({ entitledDays: 28, carryoverDays: 0 });
    const updatedEnt = makeEntitlement({
      entitledDays: 28,
      carryoverDays: 0,
      usedDays: 5,
      pendingDays: 3,
    });

    const approvedVacation = {
      startDate: new Date("2026-06-01"),
      endDate: new Date("2026-06-05"),
      status: VacationStatus.APPROVED,
      isHalfDay: false,
      deductedDays: 5,
      holidayCountryCode: null,
      holidayFederalState: null,
      holidayMetroCityName: null,
      holidayCalendarDates: null,
      holidayLegacyPublicHolidayDates: null,
    };
    const pendingVacation = {
      startDate: new Date("2026-07-01"),
      endDate: new Date("2026-07-03"),
      status: VacationStatus.PENDING,
      isHalfDay: false,
      deductedDays: 3,
      holidayCountryCode: null,
      holidayFederalState: null,
      holidayMetroCityName: null,
      holidayCalendarDates: null,
      holidayLegacyPublicHolidayDates: null,
    };

    const db = {
      vacationEntitlement: {
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(null) // prev year check
          .mockResolvedValueOnce(existingEnt), // getOrCreateEntitlement
        create: vi.fn(),
        update: vi.fn().mockResolvedValue(updatedEnt),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([approvedVacation, pendingVacation]),
      },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    const deps = makeSyncDeps({
      // snapshot returns null => falls back to countVacationChargeableDays
      countVacationChargeableDaysFromSnapshot: vi.fn().mockReturnValue(null),
      countVacationChargeableDays: vi
        .fn()
        .mockReturnValueOnce(5) // approved vacation
        .mockReturnValueOnce(3), // pending vacation
    });

    await syncEntitlement(db as never, "resource_1", 2026, 28, deps);

    const finalUpdate = (db.vacationEntitlement.update as ReturnType<typeof vi.fn>).mock.calls.at(
      -1,
    );
    expect(finalUpdate![0].data.usedDays).toBe(5);
    expect(finalUpdate![0].data.pendingDays).toBe(3);
  });

  it("uses persisted snapshot days when countVacationChargeableDaysFromSnapshot returns a non-null value", async () => {
    const { VacationStatus } = await import("@capakraken/db");
    const existingEnt = makeEntitlement({ entitledDays: 28, carryoverDays: 0 });
    const updatedEnt = makeEntitlement({ entitledDays: 28, usedDays: 7, pendingDays: 0 });

    const approvedVacation = {
      startDate: new Date("2026-08-01"),
      endDate: new Date("2026-08-07"),
      status: VacationStatus.APPROVED,
      isHalfDay: false,
      deductedDays: 7,
      holidayCountryCode: "DE",
      holidayFederalState: null,
      holidayMetroCityName: null,
      holidayCalendarDates: null,
      holidayLegacyPublicHolidayDates: null,
    };

    const db = {
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(existingEnt),
        create: vi.fn(),
        update: vi.fn().mockResolvedValue(updatedEnt),
      },
      vacation: { findMany: vi.fn().mockResolvedValue([approvedVacation]) },
      systemSettings: { findUnique: vi.fn() },
      resource: { findMany: vi.fn() },
    };

    const chargeableDaysFromSnapshot = vi.fn().mockReturnValue(7);
    const chargeableDays = vi.fn();

    const deps = makeSyncDeps({
      countVacationChargeableDaysFromSnapshot: chargeableDaysFromSnapshot,
      countVacationChargeableDays: chargeableDays,
    });

    await syncEntitlement(db as never, "resource_1", 2026, 28, deps);

    expect(chargeableDaysFromSnapshot).toHaveBeenCalled();
    // Because snapshot returned a value, the live calculator should NOT be called
    expect(chargeableDays).not.toHaveBeenCalled();

    const finalUpdate = (db.vacationEntitlement.update as ReturnType<typeof vi.fn>).mock.calls.at(
      -1,
    );
    expect(finalUpdate![0].data.usedDays).toBe(7);
  });
});

// ---------------------------------------------------------------------------
// getEntitlementBalance
// ---------------------------------------------------------------------------

describe("getEntitlementBalance", () => {
  it("returns correct remainingDays = entitled + carryover - used - pending", async () => {
    // entitledDays=30 (28 base + 2 carryover), usedDays=10, pendingDays=5 => remaining=15
    const syncedEnt = makeEntitlement({
      entitledDays: 30,
      carryoverDays: 2,
      usedDays: 10,
      pendingDays: 5,
    });

    const db = {
      systemSettings: {
        findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }),
      },
      vacationEntitlement: {
        findUnique: vi
          .fn()
          .mockResolvedValueOnce(null) // prev year check in syncEntitlement
          .mockResolvedValueOnce(syncedEnt), // getOrCreateEntitlement finds existing
        create: vi.fn(),
        update: vi.fn().mockResolvedValue(syncedEnt),
      },
      vacation: { findMany: vi.fn().mockResolvedValue([]) },
      resource: { findMany: vi.fn() },
    };

    const deps = makeReadDeps();

    const result = await getEntitlementBalance(
      db as never,
      { resourceId: "resource_1", year: 2026 },
      deps,
    );

    expect(result.entitledDays).toBe(30);
    expect(result.carryoverDays).toBe(2);
    expect(result.usedDays).toBe(10);
    expect(result.pendingDays).toBe(5);
    expect(result.remainingDays).toBe(15);
    expect(result.year).toBe(2026);
    expect(result.resourceId).toBe("resource_1");
  });

  it("clamps remainingDays to 0 when used+pending exceed entitled", async () => {
    const syncedEnt = makeEntitlement({
      entitledDays: 10,
      carryoverDays: 0,
      usedDays: 8,
      pendingDays: 5, // 8+5=13 > 10
    });

    const db = {
      systemSettings: {
        findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }),
      },
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(syncedEnt),
        create: vi.fn(),
        update: vi.fn().mockResolvedValue(syncedEnt),
      },
      vacation: { findMany: vi.fn().mockResolvedValue([]) },
      resource: { findMany: vi.fn() },
    };

    const deps = makeReadDeps();

    const result = await getEntitlementBalance(
      db as never,
      { resourceId: "resource_1", year: 2026 },
      deps,
    );

    expect(result.remainingDays).toBe(0);
  });

  it("falls back to 28 default vacation days when systemSettings is null", async () => {
    const syncedEnt = makeEntitlement({
      entitledDays: 28,
      carryoverDays: 0,
      usedDays: 0,
      pendingDays: 0,
    });

    const db = {
      systemSettings: {
        findUnique: vi.fn().mockResolvedValue(null), // no settings record
      },
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(null), // force creation path
        create: vi.fn().mockResolvedValue(syncedEnt),
        update: vi.fn().mockResolvedValue(syncedEnt),
      },
      vacation: { findMany: vi.fn().mockResolvedValue([]) },
      resource: { findMany: vi.fn() },
    };

    const deps = makeReadDeps();

    // Should not throw — falls back to 28 default days
    const result = await getEntitlementBalance(
      db as never,
      { resourceId: "resource_1", year: 2026 },
      deps,
    );

    expect(result.entitledDays).toBe(28);
  });

  it("counts sick days separately from annual leave balance", async () => {
    const { VacationStatus } = await import("@capakraken/db");
    const syncedEnt = makeEntitlement({
      entitledDays: 28,
      carryoverDays: 0,
      usedDays: 0,
      pendingDays: 0,
    });
    const sickVacation = {
      startDate: new Date("2026-03-01"),
      endDate: new Date("2026-03-03"),
      isHalfDay: false,
    };

    const db = {
      systemSettings: {
        findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }),
      },
      vacationEntitlement: {
        findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(syncedEnt),
        create: vi.fn(),
        update: vi.fn().mockResolvedValue(syncedEnt),
      },
      vacation: {
        // First call: annual leave vacations for syncEntitlement => empty
        // Second call: sick vacations for readBalanceSnapshot
        findMany: vi
          .fn()
          .mockResolvedValueOnce([]) // annual balance vacations
          .mockResolvedValueOnce([sickVacation]), // sick vacations
      },
      resource: { findMany: vi.fn() },
    };

    const countCalendarDaysInPeriod = vi.fn().mockReturnValue(3);
    const deps = makeReadDeps({ countCalendarDaysInPeriod });

    const result = await getEntitlementBalance(
      db as never,
      { resourceId: "resource_1", year: 2026 },
      deps,
    );

    expect(result.sickDays).toBe(3);
    // Sick days should not reduce remaining annual leave
    expect(result.remainingDays).toBe(28);
  });
});
