import { AllocationStatus, SystemRole } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { allocationRouter } from "../router/allocation/index.js";
import { createCallerFactory } from "../trpc.js";

vi.mock("../sse/event-bus.js", () => ({
  emitAllocationCreated: vi.fn(),
  emitAllocationDeleted: vi.fn(),
  emitAllocationUpdated: vi.fn(),
  emitNotificationCreated: vi.fn(),
}));

vi.mock("../lib/budget-alerts.js", () => ({
  checkBudgetThresholds: vi.fn(),
}));

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

vi.mock("../lib/auto-staffing.js", () => ({
  generateAutoSuggestions: vi.fn(),
}));

vi.mock("../lib/webhook-dispatcher.js", () => ({
  dispatchWebhooks: 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(allocationRouter);

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

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: "user_1",
      systemRole: SystemRole.MANAGER,
      permissionOverrides: null,
    },
  });
}

/** Monday–Friday week, all days 8h/day capacity */
const BASE_AVAILABILITY = {
  monday: 8,
  tuesday: 8,
  wednesday: 8,
  thursday: 8,
  friday: 8,
  saturday: 0,
  sunday: 0,
};

const RESOURCE_BASE = {
  availability: BASE_AVAILABILITY,
  fte: 1,
  country: { dailyWorkingHours: 8 },
};

describe("allocation.checkConflicts", () => {
  it("returns no conflict when the resource has no existing assignments", async () => {
    const db = {
      resource: {
        findUnique: vi.fn().mockResolvedValue(RESOURCE_BASE),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const caller = createManagerCaller(db);
    const result = await caller.checkConflicts({
      resourceId: "res_1",
      startDate: new Date("2026-05-04T00:00:00.000Z"), // Monday
      endDate: new Date("2026-05-08T00:00:00.000Z"),   // Friday
      hoursPerDay: 8,
    });

    expect(result.isOverbooking).toBe(false);
    expect(result.overbooking).toBeNull();
    expect(result.hasVacationOverlap).toBe(false);
    expect(result.vacationOverlap).toHaveLength(0);
  });

  it("detects overbooking when existing + requested hours exceed daily capacity", async () => {
    // 2026-05-04 (Mon) – 2026-05-08 (Fri) = 5 working days
    // Existing: 4h/day → requesting another 6h = 10h total on 8h capacity
    const db = {
      resource: {
        findUnique: vi.fn().mockResolvedValue(RESOURCE_BASE),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([
          {
            startDate: new Date("2026-05-04T00:00:00.000Z"),
            endDate: new Date("2026-05-08T00:00:00.000Z"),
            hoursPerDay: 4,
            status: AllocationStatus.ACTIVE,
          },
        ]),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const caller = createManagerCaller(db);
    const result = await caller.checkConflicts({
      resourceId: "res_1",
      startDate: new Date("2026-05-04T00:00:00.000Z"),
      endDate: new Date("2026-05-08T00:00:00.000Z"),
      hoursPerDay: 6,
    });

    expect(result.isOverbooking).toBe(true);
    expect(result.overbooking).not.toBeNull();
    // Mon–Fri = 5 conflict days
    expect(result.overbooking!.totalConflictDays).toBe(5);
    expect(result.overbooking!.conflictDays).toHaveLength(5);
    // Each day: 4 existing + 6 requested = 10 > 8 → overage 2h
    const day = result.overbooking!.conflictDays[0]!;
    expect(day.existingHours).toBe(4);
    expect(day.requestedHours).toBe(6);
    expect(day.availableHours).toBe(8);
    expect(day.overageHours).toBeCloseTo(2);
    // maxOverbookPercent: (10/8 - 1) * 100 = 25%
    expect(result.overbooking!.maxOverbookPercent).toBe(25);
    expect(result.hasVacationOverlap).toBe(false);
  });

  it("reports vacation overlap when the resource has approved leave in the period", async () => {
    const db = {
      resource: {
        findUnique: vi.fn().mockResolvedValue(RESOURCE_BASE),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([
          {
            startDate: new Date("2026-05-07T00:00:00.000Z"),
            endDate: new Date("2026-05-07T00:00:00.000Z"),
            type: "ANNUAL_LEAVE",
            isHalfDay: false,
          },
        ]),
      },
    };

    const caller = createManagerCaller(db);
    const result = await caller.checkConflicts({
      resourceId: "res_1",
      startDate: new Date("2026-05-04T00:00:00.000Z"),
      endDate: new Date("2026-05-08T00:00:00.000Z"),
      hoursPerDay: 4,
    });

    expect(result.isOverbooking).toBe(false);
    expect(result.hasVacationOverlap).toBe(true);
    expect(result.vacationOverlap).toHaveLength(1);
    expect(result.vacationOverlap[0]!.type).toBe("ANNUAL_LEAVE");
    expect(result.vacationOverlap[0]!.startDate).toBe("2026-05-07");
    expect(result.vacationOverlap[0]!.endDate).toBe("2026-05-07");
    expect(result.vacationOverlap[0]!.isHalfDay).toBe(false);
  });

  it("excludes the current assignment when excludeAssignmentId is provided (edit mode)", async () => {
    // Editing assignment asn_current (8h/day). Without exclusion it would double-count itself.
    // With exclusion the existing load is 0 → no overbooking for 8h request.
    const db = {
      resource: {
        findUnique: vi.fn().mockResolvedValue(RESOURCE_BASE),
      },
      assignment: {
        // Simulate that Prisma respects the `id: { not: excludeAssignmentId }` filter
        findMany: vi.fn().mockResolvedValue([]),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const caller = createManagerCaller(db);
    const result = await caller.checkConflicts({
      resourceId: "res_1",
      startDate: new Date("2026-05-04T00:00:00.000Z"),
      endDate: new Date("2026-05-08T00:00:00.000Z"),
      hoursPerDay: 8,
      excludeAssignmentId: "asn_current",
    });

    // Verify the exclusion filter was passed to Prisma
    expect(db.assignment.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          id: { not: "asn_current" },
        }),
      }),
    );

    expect(result.isOverbooking).toBe(false);
  });

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

    const caller = createManagerCaller(db);
    await expect(
      caller.checkConflicts({
        resourceId: "nonexistent",
        startDate: new Date("2026-05-05T00:00:00.000Z"),
        endDate: new Date("2026-05-09T00:00:00.000Z"),
        hoursPerDay: 4,
      }),
    ).rejects.toThrow("Resource not found");
  });

  it("uses country working hours as fallback when resource has no availability set", async () => {
    const db = {
      resource: {
        findUnique: vi.fn().mockResolvedValue({
          availability: null,
          fte: 1,
          country: { dailyWorkingHours: 6 },
        }),
      },
      assignment: {
        // Existing 5h/day → requesting 4h = 9h > 6h fallback capacity
        findMany: vi.fn().mockResolvedValue([
          {
            startDate: new Date("2026-05-05T00:00:00.000Z"),
            endDate: new Date("2026-05-05T00:00:00.000Z"),
            hoursPerDay: 5,
            status: AllocationStatus.ACTIVE,
          },
        ]),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const caller = createManagerCaller(db);
    const result = await caller.checkConflicts({
      resourceId: "res_1",
      startDate: new Date("2026-05-05T00:00:00.000Z"),
      endDate: new Date("2026-05-05T00:00:00.000Z"),
      hoursPerDay: 4,
    });

    expect(result.isOverbooking).toBe(true);
    expect(result.overbooking!.conflictDays[0]!.availableHours).toBe(6);
  });
});
