import { expect, test } from "bun:test";
import {
  combinedProgress,
  demoCalendar,
  describeDay,
  progressShade,
  PROGRESS_SHADES,
  monthWindow,
  visibleCalendarDays,
  MONTH_VIEWS,
  legendShades,
  calendarTimeline,
} from "./calendar-model";

test("timeline keeps month boundaries in the same week and covers leap days and gaps", () => {
  const days = ["2024-02-28", "2024-02-29", "2024-03-01", "2024-03-03", "2024-03-10"]
    .map((date) => ({ date, value: 0, target: 1, state: "due" as const }));
  const timeline = calendarTimeline(days);
  expect(timeline.weeks).toBe(3);
  expect(timeline.entries.map(({ column, row }) => [column, row])).toEqual([
    [1, 4], [1, 5], [1, 6], [2, 1], [3, 1],
  ]);
  expect(calendarTimeline([])).toEqual({ weeks: 0, entries: [], months: [] });
  const year = calendarTimeline(demoCalendar(7, 8));
  expect(year.weeks).toBe(53);
  expect(year.months).toHaveLength(12);
  expect(year.months[3]?.month).toBe("2026-01");
});

test("legends match habit colors and only display possible progress shades", () => {
  for (const color of ["#111111", "#426582", "#977344", "#79618d", "#58765b"]) {
    for (const target of [1, 3, 4, 8, 20]) {
      const shades = legendShades(demoCalendar(0, target), color);
      expect(shades).toHaveLength(Math.min(target, 8) + 1);
      expect(shades[0]).toBe(PROGRESS_SHADES[0]);
      expect(shades.at(-1)).toBe(color);
      for (let value = 0; value <= target; value++) {
        expect(shades).toContain(
          progressShade({ date: "", value, target, state: "due" }, color),
        );
      }
    }
  }
  expect(legendShades(demoCalendar(0, 0))).toEqual([PROGRESS_SHADES[0]]);
  expect(legendShades([])).toEqual([PROGRESS_SHADES[0]]);
});

test("eight glasses have eight positive shades plus empty; 7/8 remains incomplete", () => {
  const days = Array.from({ length: 9 }, (_, value) => ({
    date: "2026-09-04",
    value,
    target: 8,
    state: "due" as const,
  }));
  expect(new Set(days.map((day) => progressShade(day))).size).toBe(9);
  for (const color of ["#426582", "#977344", "#79618d", "#58765b"]) {
    expect(new Set(days.map((day) => progressShade(day, color))).size).toBe(9);
    expect(progressShade(days[8]!, color)).toBe(color);
  }
  expect(describeDay(days[7]!, "glasses")).toBe("7 of 8 glasses · Incomplete");
  expect(progressShade({ ...days[8]!, value: 10 })).toBe(PROGRESS_SHADES[8]);
});

test("combined progress counts only completed due habits with equal weights", () => {
  expect(
    combinedProgress([
      { value: 7, target: 8, due: true },
      { value: 1, target: 1, due: true },
      { value: 2, target: 3, due: true },
      { value: 0, target: 1, due: false },
    ]),
  ).toEqual({ completed: 1, due: 3 });
  expect(combinedProgress([{ value: 0, target: 0, due: true }])).toEqual({
    completed: 0,
    due: 0,
  });
});

test("future and zero share a fill while inspection retains their distinct meaning", () => {
  const days = demoCalendar(7, 8);
  expect(days).toHaveLength(365);
  expect(days.find((day) => day.date === "2026-09-04")?.value).toBe(7);
  expect(
    describeDay(
      days.find((day) => day.state === "not-due")!,
      "glasses",
    ),
  ).toContain("Nothing scheduled");
  expect(describeDay(days.at(-1)!, "glasses")).toContain("Upcoming");
  const zero = {
    date: "2026-09-04",
    value: 0,
    target: 8,
    state: "due" as const,
  };
  for (const color of ["#111111", "#426582", "#977344", "#79618d", "#58765b"]) {
    expect(progressShade(days.at(-1)!, color)).toBe(progressShade(zero, color));
    expect(progressShade(days.at(-1)!, color)).toBe(PROGRESS_SHADES[0]);
  }
  expect(describeDay(zero, "glasses")).toBe("0 of 8 glasses · Incomplete");
});

test("month presets include exact whole months and cross year and leap-year boundaries", () => {
  expect(
    MONTH_VIEWS.map((months) => monthWindow("2026-09-04", months)),
  ).toEqual([
    { from: "2026-07-01", to: "2026-09-30" },
    { from: "2026-06-01", to: "2026-09-30" },
    { from: "2026-04-01", to: "2026-09-30" },
    { from: "2025-10-01", to: "2026-09-30" },
  ]);
  expect(monthWindow("2024-02-29", 3)).toEqual({
    from: "2023-12-01",
    to: "2024-02-29",
  });
});

test("switching month views preserves dated values, complete coverage and future states", () => {
  const days = demoCalendar(7, 8);
  expect(
    MONTH_VIEWS.map(
      (months) => visibleCalendarDays(days, months, "2026-09-04").length,
    ),
  ).toEqual([92, 122, 183, 365]);
  for (const months of MONTH_VIEWS) {
    const visible = visibleCalendarDays(days, months, "2026-09-04");
    expect(visible.find((day) => day.date === "2026-09-04")).toEqual(
      days.find((day) => day.date === "2026-09-04"),
    );
    expect(visible.filter((day) => day.state === "future")).toHaveLength(26);
    expect(new Set(visible.map((day) => day.date)).size).toBe(visible.length);
  }
  expect(visibleCalendarDays([], 3, "2026-09-04")).toEqual([]);
});

test("live calendars preserve server colors and historical units", () => {
  const day = { date: "2026-09-03", value: 3, target: 8, state: "due" as const, color: "#abcdef", unit: "pages" };
  expect(progressShade(day, "#111111")).toBe("#abcdef");
  expect(describeDay(day, "glasses")).toBe("3 of 8 pages · Incomplete");
});
