import { describe, expect, it } from "vitest";
import {
  deriveResourceForecast,
  calculateGroupChargeability,
  calculateGroupTarget,
  sumFte,
  getMonthRange,
  getMonthKeys,
  countWorkingDaysInOverlap,
} from "../chargeability/calculator.js";

// ─── deriveResourceForecast ──────────────────────────────────────────────────

describe("deriveResourceForecast", () => {
  it("returns unassigned=1 when sah=0", () => {
    const result = deriveResourceForecast({
      fte: 1,
      targetPercentage: 0.8,
      assignments: [],
      sah: 0,
    });
    expect(result).toEqual({ chg: 0, bd: 0, mdi: 0, mo: 0, pdr: 0, absence: 0, unassigned: 1 });
  });

  it("fully assigned to chargeable work", () => {
    const result = deriveResourceForecast({
      fte: 1,
      targetPercentage: 0.8,
      assignments: [{ hoursPerDay: 8, workingDays: 20, categoryCode: "Chg" }],
      sah: 160, // 8h * 20d
    });
    expect(result.chg).toBeCloseTo(1, 5);
    expect(result.unassigned).toBeCloseTo(0, 5);
  });

  it("50% chargeable, 25% BD, rest unassigned", () => {
    const result = deriveResourceForecast({
      fte: 1,
      targetPercentage: 0.8,
      assignments: [
        { hoursPerDay: 4, workingDays: 20, categoryCode: "Chg" },
        { hoursPerDay: 2, workingDays: 20, categoryCode: "BD" },
      ],
      sah: 160,
    });
    expect(result.chg).toBeCloseTo(0.5, 5);
    expect(result.bd).toBeCloseTo(0.25, 5);
    expect(result.unassigned).toBeCloseTo(0.25, 5);
  });

  it("handles case-insensitive category codes", () => {
    const result = deriveResourceForecast({
      fte: 1,
      targetPercentage: 0.8,
      assignments: [
        { hoursPerDay: 8, workingDays: 10, categoryCode: "chg" },
        { hoursPerDay: 8, workingDays: 10, categoryCode: "CHG" },
      ],
      sah: 160,
    });
    expect(result.chg).toBeCloseTo(1, 5);
  });

  it("handles MD&I and M&O codes with ampersand", () => {
    const result = deriveResourceForecast({
      fte: 1,
      targetPercentage: 0.8,
      assignments: [
        { hoursPerDay: 4, workingDays: 20, categoryCode: "MD&I" },
        { hoursPerDay: 4, workingDays: 20, categoryCode: "M&O" },
      ],
      sah: 160,
    });
    expect(result.mdi).toBeCloseTo(0.5, 5);
    expect(result.mo).toBeCloseTo(0.5, 5);
  });

  it("handles PD&R code", () => {
    const result = deriveResourceForecast({
      fte: 1,
      targetPercentage: 0.8,
      assignments: [{ hoursPerDay: 8, workingDays: 20, categoryCode: "PD&R" }],
      sah: 160,
    });
    expect(result.pdr).toBeCloseTo(1, 5);
  });

  it("handles absence category", () => {
    const result = deriveResourceForecast({
      fte: 1,
      targetPercentage: 0.8,
      assignments: [
        { hoursPerDay: 4, workingDays: 20, categoryCode: "Chg" },
        { hoursPerDay: 4, workingDays: 20, categoryCode: "Absence" },
      ],
      sah: 160,
    });
    expect(result.chg).toBeCloseTo(0.5, 5);
    expect(result.absence).toBeCloseTo(0.5, 5);
    expect(result.unassigned).toBeCloseTo(0, 5);
  });

  it("clamps ratios at 1 when over-assigned", () => {
    const result = deriveResourceForecast({
      fte: 1,
      targetPercentage: 0.8,
      assignments: [{ hoursPerDay: 16, workingDays: 20, categoryCode: "Chg" }],
      sah: 160,
    });
    expect(result.chg).toBe(1);
  });

  it("multiple assignments same category are summed", () => {
    const result = deriveResourceForecast({
      fte: 0.5,
      targetPercentage: 0.8,
      assignments: [
        { hoursPerDay: 2, workingDays: 20, categoryCode: "Chg" },
        { hoursPerDay: 2, workingDays: 20, categoryCode: "Chg" },
      ],
      sah: 80,
    });
    expect(result.chg).toBeCloseTo(1, 5);
  });
});

// ─── calculateGroupChargeability ─────────────────────────────────────────────

describe("calculateGroupChargeability", () => {
  it("returns 0 for empty group", () => {
    expect(calculateGroupChargeability([])).toBe(0);
  });

  it("single resource returns its chargeability", () => {
    expect(calculateGroupChargeability([{ fte: 1, chargeability: 0.75 }])).toBeCloseTo(0.75, 5);
  });

  it("FTE-weighted average of two resources", () => {
    const result = calculateGroupChargeability([
      { fte: 1, chargeability: 0.8 },
      { fte: 0.5, chargeability: 0.6 },
    ]);
    // (1*0.8 + 0.5*0.6) / (1 + 0.5) = 1.1 / 1.5 = 0.7333...
    expect(result).toBeCloseTo(0.7333, 3);
  });

  it("returns 0 when all FTEs are 0", () => {
    expect(calculateGroupChargeability([{ fte: 0, chargeability: 0.9 }])).toBe(0);
  });
});

// ─── calculateGroupTarget ────────────────────────────────────────────────────

describe("calculateGroupTarget", () => {
  it("returns 0 for empty group", () => {
    expect(calculateGroupTarget([])).toBe(0);
  });

  it("FTE-weighted target average", () => {
    const result = calculateGroupTarget([
      { fte: 1, targetPercentage: 0.8 },
      { fte: 1, targetPercentage: 0.6 },
    ]);
    expect(result).toBeCloseTo(0.7, 5);
  });
});

// ─── sumFte ──────────────────────────────────────────────────────────────────

describe("sumFte", () => {
  it("sums FTE values", () => {
    expect(sumFte([{ fte: 1 }, { fte: 0.5 }, { fte: 0.75 }])).toBeCloseTo(2.25, 5);
  });

  it("returns 0 for empty array", () => {
    expect(sumFte([])).toBe(0);
  });
});

// ─── getMonthRange ───────────────────────────────────────────────────────────

describe("getMonthRange", () => {
  it("January 2026", () => {
    const { start, end } = getMonthRange(2026, 1);
    expect(start.toISOString()).toBe("2026-01-01T00:00:00.000Z");
    expect(end.toISOString()).toBe("2026-01-31T00:00:00.000Z");
  });

  it("February 2024 (leap year)", () => {
    const { start, end } = getMonthRange(2024, 2);
    expect(start.toISOString()).toBe("2024-02-01T00:00:00.000Z");
    expect(end.toISOString()).toBe("2024-02-29T00:00:00.000Z");
  });

  it("February 2025 (non-leap year)", () => {
    const { start, end } = getMonthRange(2025, 2);
    expect(end.toISOString()).toBe("2025-02-28T00:00:00.000Z");
  });
});

// ─── getMonthKeys ────────────────────────────────────────────────────────────

describe("getMonthKeys", () => {
  it("generates keys for Q1 2026", () => {
    const keys = getMonthKeys(new Date("2026-01-01"), new Date("2026-03-31"));
    expect(keys).toEqual(["2026-01", "2026-02", "2026-03"]);
  });

  it("single month", () => {
    const keys = getMonthKeys(new Date("2026-06-15"), new Date("2026-06-20"));
    expect(keys).toEqual(["2026-06"]);
  });

  it("cross-year boundary", () => {
    const keys = getMonthKeys(new Date("2025-11-01"), new Date("2026-02-28"));
    expect(keys).toEqual(["2025-11", "2025-12", "2026-01", "2026-02"]);
  });
});

// ─── countWorkingDaysInOverlap ───────────────────────────────────────────────

describe("countWorkingDaysInOverlap", () => {
  it("full overlap on a workweek = 5 days", () => {
    const mon = new Date("2026-01-05");
    const fri = new Date("2026-01-09");
    expect(countWorkingDaysInOverlap(mon, fri, mon, fri)).toBe(5);
  });

  it("no overlap returns 0", () => {
    expect(
      countWorkingDaysInOverlap(
        new Date("2026-01-05"),
        new Date("2026-01-09"),
        new Date("2026-01-12"),
        new Date("2026-01-16"),
      ),
    ).toBe(0);
  });

  it("partial overlap: assignment starts mid-week", () => {
    // Period Mon-Fri, assignment Wed-next Mon → overlap Wed-Fri = 3
    expect(
      countWorkingDaysInOverlap(
        new Date("2026-01-05"), // Mon
        new Date("2026-01-09"), // Fri
        new Date("2026-01-07"), // Wed
        new Date("2026-01-12"), // next Mon
      ),
    ).toBe(3);
  });

  it("excludes weekends from overlap", () => {
    // Period: Mon-next Fri (10 calendar days, 10 working days)
    // Assignment: entire same range
    expect(
      countWorkingDaysInOverlap(
        new Date("2026-01-05"), // Mon
        new Date("2026-01-16"), // Fri
        new Date("2026-01-05"),
        new Date("2026-01-16"),
      ),
    ).toBe(10);
  });

  it("weekend-only overlap returns 0", () => {
    expect(
      countWorkingDaysInOverlap(
        new Date("2026-01-10"), // Sat
        new Date("2026-01-11"), // Sun
        new Date("2026-01-10"),
        new Date("2026-01-11"),
      ),
    ).toBe(0);
  });
});
