import { describe, expect, it } from "vitest";
import {
  generateWeekRange,
  distributeHoursToWeeks,
  weekKeyFromDate,
  aggregateWeeklyToMonthly,
  aggregateWeeklyByChapter,
} from "../estimate/weekly-phasing.js";

describe("generateWeekRange", () => {
  it("returns a single week for a range within one week", () => {
    const weeks = generateWeekRange("2026-03-16", "2026-03-20"); // Mon-Fri of W12
    expect(weeks).toHaveLength(1);
    expect(weeks[0]!.weekNumber).toBe(12);
    expect(weeks[0]!.year).toBe(2026);
    expect(weeks[0]!.label).toBe("W12 2026");
  });

  it("returns multiple weeks for a multi-week range", () => {
    const weeks = generateWeekRange("2026-03-16", "2026-04-10");
    expect(weeks.length).toBeGreaterThanOrEqual(3);
    // Should cover W12 through at least W15
    const weekNumbers = weeks.map((w) => w.weekNumber);
    expect(weekNumbers).toContain(12);
    expect(weekNumbers).toContain(13);
    expect(weekNumbers).toContain(14);
  });

  it("handles month boundary crossing", () => {
    const weeks = generateWeekRange("2026-03-28", "2026-04-05");
    // This spans end of March into April
    expect(weeks.length).toBeGreaterThanOrEqual(1);
    // Each week has proper start/end dates
    for (const week of weeks) {
      expect(week.startDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
      expect(week.endDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
    }
  });

  it("handles year boundary crossing", () => {
    const weeks = generateWeekRange("2025-12-22", "2026-01-11");
    expect(weeks.length).toBeGreaterThanOrEqual(2);
    // Should include weeks in both years (ISO week year may differ from calendar year)
    const labels = weeks.map((w) => w.label);
    expect(labels.length).toBeGreaterThan(0);
  });

  it("returns correct ISO week numbers and labels", () => {
    const weeks = generateWeekRange("2026-01-05", "2026-01-18");
    for (const week of weeks) {
      expect(week.label).toMatch(/^W\d{2} \d{4}$/);
      expect(week.weekNumber).toBeGreaterThanOrEqual(1);
      expect(week.weekNumber).toBeLessThanOrEqual(53);
    }
  });

  it("returns empty array when endDate < startDate", () => {
    const weeks = generateWeekRange("2026-06-01", "2026-03-01");
    expect(weeks).toEqual([]);
  });

  it("returns week definitions with Monday start and Sunday end", () => {
    const weeks = generateWeekRange("2026-03-16", "2026-03-22");
    const week = weeks[0]!;
    const start = new Date(week.startDate);
    const end = new Date(week.endDate);
    expect(start.getUTCDay()).toBe(1); // Monday
    expect(end.getUTCDay()).toBe(0); // Sunday
  });
});

describe("distributeHoursToWeeks", () => {
  it("distributes hours evenly across weeks", () => {
    const result = distributeHoursToWeeks({
      totalHours: 100,
      startDate: "2026-03-16",
      endDate: "2026-04-10",
      pattern: "even",
    });

    expect(result.weeks.length).toBeGreaterThan(0);
    expect(result.totalDistributedHours).toBeCloseTo(100, 1);
    // All weeks should have roughly equal hours
    const values = Object.values(result.weeklyHours);
    const avg = 100 / values.length;
    for (const h of values) {
      expect(h).toBeCloseTo(avg, 0);
    }
  });

  it("distributes 100h over 4 weeks = 25h each", () => {
    // Find a 4-week range
    const result = distributeHoursToWeeks({
      totalHours: 100,
      startDate: "2026-03-16",
      endDate: "2026-04-12",
      pattern: "even",
    });

    expect(result.weeks).toHaveLength(4);
    expect(result.totalDistributedHours).toBe(100);
    for (const h of Object.values(result.weeklyHours)) {
      expect(h).toBe(25);
    }
  });

  it("handles even distribution with remainder", () => {
    const result = distributeHoursToWeeks({
      totalHours: 100,
      startDate: "2026-03-16",
      endDate: "2026-04-05",
      pattern: "even",
    });

    expect(result.weeks).toHaveLength(3);
    expect(result.totalDistributedHours).toBeCloseTo(100, 1);
  });

  it("front loaded: first half weeks have more hours", () => {
    const result = distributeHoursToWeeks({
      totalHours: 100,
      startDate: "2026-03-16",
      endDate: "2026-05-10",
      pattern: "front_loaded",
    });

    const values = Object.values(result.weeklyHours);
    expect(values.length).toBeGreaterThanOrEqual(4);

    // First value should be greater than last value
    expect(values[0]!).toBeGreaterThan(values[values.length - 1]!);
    expect(result.totalDistributedHours).toBeCloseTo(100, 1);
  });

  it("back loaded: last half weeks have more hours", () => {
    const result = distributeHoursToWeeks({
      totalHours: 100,
      startDate: "2026-03-16",
      endDate: "2026-05-10",
      pattern: "back_loaded",
    });

    const values = Object.values(result.weeklyHours);
    expect(values.length).toBeGreaterThanOrEqual(4);

    // Last value should be greater than first value
    expect(values[values.length - 1]!).toBeGreaterThan(values[0]!);
    expect(result.totalDistributedHours).toBeCloseTo(100, 1);
  });

  it("custom: uses provided hours", () => {
    const result = distributeHoursToWeeks({
      totalHours: 100,
      startDate: "2026-03-16",
      endDate: "2026-04-05",
      pattern: "custom",
      customWeeklyHours: {
        "2026-W12": 40,
        "2026-W13": 35,
        "2026-W14": 25,
      },
    });

    expect(result.weeklyHours["2026-W12"]).toBe(40);
    expect(result.weeklyHours["2026-W13"]).toBe(35);
    expect(result.weeklyHours["2026-W14"]).toBe(25);
  });

  it("returns all zeros for zero hours", () => {
    const result = distributeHoursToWeeks({
      totalHours: 0,
      startDate: "2026-03-16",
      endDate: "2026-04-05",
      pattern: "even",
    });

    for (const h of Object.values(result.weeklyHours)) {
      expect(h).toBe(0);
    }
    expect(result.totalDistributedHours).toBe(0);
  });

  it("handles single week range", () => {
    const result = distributeHoursToWeeks({
      totalHours: 40,
      startDate: "2026-03-16",
      endDate: "2026-03-20",
      pattern: "front_loaded",
    });

    expect(result.weeks).toHaveLength(1);
    expect(result.totalDistributedHours).toBe(40);
  });

  it("returns empty result for empty range", () => {
    const result = distributeHoursToWeeks({
      totalHours: 100,
      startDate: "2026-06-01",
      endDate: "2026-03-01",
    });

    expect(result.weeks).toHaveLength(0);
    expect(result.totalDistributedHours).toBe(0);
  });

  it("defaults to even pattern when not specified", () => {
    const result = distributeHoursToWeeks({
      totalHours: 80,
      startDate: "2026-03-16",
      endDate: "2026-04-12",
    });

    expect(result.weeks).toHaveLength(4);
    expect(result.totalDistributedHours).toBeCloseTo(80, 1);
    // Should be even
    for (const h of Object.values(result.weeklyHours)) {
      expect(h).toBe(20);
    }
  });
});

describe("weekKeyFromDate", () => {
  it("returns correct week key for a normal date", () => {
    // March 18, 2026 is a Wednesday in W12
    expect(weekKeyFromDate("2026-03-18")).toBe("2026-W12");
  });

  it("handles Date object input", () => {
    const date = new Date("2026-03-18");
    expect(weekKeyFromDate(date)).toBe("2026-W12");
  });

  it("handles year boundary (Dec 31 may be W01 of next year)", () => {
    // Dec 31, 2026 is a Thursday. Let's check ISO week.
    const key = weekKeyFromDate("2026-12-31");
    // ISO week 53 or W01 of 2027 depending on the year
    expect(key).toMatch(/^\d{4}-W\d{2}$/);
  });

  it("handles Jan 1 which may be in previous year's last week", () => {
    // Jan 1, 2027 is a Friday
    const key = weekKeyFromDate("2027-01-01");
    expect(key).toMatch(/^\d{4}-W\d{2}$/);
  });

  it("returns W01 for the first Monday of a standard year", () => {
    // In 2026, Jan 5 is a Monday, should be W02 (Jan 1 is Thursday, W01 starts Dec 29)
    // Actually let's verify: Jan 1 2026 is Thursday → W01 starts Mon Dec 29 2025
    // Jan 5 2026 is Monday → W02
    expect(weekKeyFromDate("2026-01-05")).toBe("2026-W02");
  });
});

describe("aggregateWeeklyToMonthly", () => {
  it("converts week keys to month keys and sums hours", () => {
    const weeklyHours: Record<string, number> = {
      "2026-W12": 40,
      "2026-W13": 40,
      "2026-W14": 40,
      "2026-W15": 40,
    };

    const monthly = aggregateWeeklyToMonthly(weeklyHours);
    // All these weeks are in March-April 2026
    const totalMonthly = Object.values(monthly).reduce((sum, h) => sum + h, 0);
    expect(totalMonthly).toBeCloseTo(160, 1);
  });

  it("sums hours within the same month", () => {
    // W10 and W11 of 2026 are both in March
    const weeklyHours: Record<string, number> = {
      "2026-W10": 20,
      "2026-W11": 30,
    };

    const monthly = aggregateWeeklyToMonthly(weeklyHours);
    // Both weeks' Thursdays fall in March
    expect(monthly["2026-03"]).toBe(50);
  });

  it("handles cross-month weeks correctly", () => {
    // W14 of 2026: Thursday April 2 → April
    const weeklyHours: Record<string, number> = {
      "2026-W13": 25,
      "2026-W14": 25,
    };

    const monthly = aggregateWeeklyToMonthly(weeklyHours);
    const total = Object.values(monthly).reduce((sum, h) => sum + h, 0);
    expect(total).toBe(50);
  });

  it("returns empty for empty input", () => {
    expect(aggregateWeeklyToMonthly({})).toEqual({});
  });
});

describe("aggregateWeeklyByChapter", () => {
  it("groups lines by chapter and sums hours per week", () => {
    const lines = [
      { chapter: "Animation", weeklyHours: { "2026-W12": 20, "2026-W13": 15 } },
      { chapter: "Animation", weeklyHours: { "2026-W12": 10, "2026-W13": 5 } },
      { chapter: "Lighting", weeklyHours: { "2026-W12": 8, "2026-W13": 12 } },
    ];

    const result = aggregateWeeklyByChapter(lines);

    expect(result["Animation"]!["2026-W12"]).toBe(30);
    expect(result["Animation"]!["2026-W13"]).toBe(20);
    expect(result["Lighting"]!["2026-W12"]).toBe(8);
    expect(result["Lighting"]!["2026-W13"]).toBe(12);
  });

  it("handles null chapter as (Unassigned)", () => {
    const lines = [
      { chapter: null, weeklyHours: { "2026-W12": 10 } },
      { weeklyHours: { "2026-W12": 5 } },
    ];

    const result = aggregateWeeklyByChapter(lines);

    expect(result["(Unassigned)"]!["2026-W12"]).toBe(15);
  });

  it("returns empty for empty input", () => {
    expect(aggregateWeeklyByChapter([])).toEqual({});
  });

  it("handles lines with no overlapping weeks", () => {
    const lines = [
      { chapter: "FX", weeklyHours: { "2026-W12": 10 } },
      { chapter: "FX", weeklyHours: { "2026-W15": 20 } },
    ];

    const result = aggregateWeeklyByChapter(lines);

    expect(result["FX"]!["2026-W12"]).toBe(10);
    expect(result["FX"]!["2026-W15"]).toBe(20);
    expect(result["FX"]!["2026-W13"]).toBeUndefined();
  });
});
