import { describe, expect, it } from "vitest";
import {
  computeOccp,
  computeOccpSafe,
  determineYearLevel,
  OccpCalculationError,
  roundForDisplay,
  yearLevelUnitBoundaries,
  type AssessmentRow,
} from "@/lib/calculations/occp";
import type { AssessmentStatus } from "@/types";

/** Builds a curriculum of `count` requirements of `units` each. */
function curriculum(count: number, units: number): AssessmentRow[] {
  return Array.from({ length: count }, (_, i) => ({
    curriculumCourseId: `course-${i + 1}`,
    curriculumUnits: units,
    status: "NOT_TAKEN" as AssessmentStatus,
    countsTowardProgramUnits: true,
  }));
}

/** Marks the first `n` requirements with the given status. */
function mark(rows: AssessmentRow[], n: number, status: AssessmentStatus): AssessmentRow[] {
  return rows.map((row, i) => (i < n ? { ...row, status } : row));
}

const TOTAL = 200;
/** 100 requirements of 2 units each = the spec's 200-unit example curriculum. */
const BASE = () => curriculum(100, 2);

describe("determineYearLevel — SPEC 4 classification", () => {
  it.each([
    [0, "1st Year"],
    [24, "1st Year"],
    [24.999, "1st Year"],
    [25, "2nd Year"],
    [49, "2nd Year"],
    [49.999, "2nd Year"],
    [50, "3rd Year"],
    [74, "3rd Year"],
    [74.999, "3rd Year"],
    [75, "4th Year"],
    [100, "4th Year"],
  ])("%s%% => %s", (percent, expected) => {
    expect(determineYearLevel(percent as number)).toBe(expected);
  });

  it("rejects a negative percentage", () => {
    expect(() => determineYearLevel(-0.01)).toThrow(OccpCalculationError);
  });
});

describe("computeOccp — SPEC 20 boundary table (200-unit curriculum)", () => {
  it.each([
    [0, 0, "1st Year"],
    [48, 24, "1st Year"],
    [50, 25, "2nd Year"],
    [98, 49, "2nd Year"],
    [100, 50, "3rd Year"],
    [148, 74, "3rd Year"],
    [150, 75, "4th Year"],
    [200, 100, "4th Year"],
  ])("%s credited units => %s%% => %s", (creditedUnits, percent, yearLevel) => {
    const rows = mark(BASE(), (creditedUnits as number) / 2, "PASSED");
    const result = computeOccp(TOTAL, rows);
    expect(result.creditedUnits).toBe(creditedUnits);
    expect(result.displayPercentage).toBe(percent);
    expect(result.rawPercentage).toBeCloseTo(percent as number, 10);
    expect(result.yearLevel).toBe(yearLevel);
  });
});

describe("computeOccp — decimals near thresholds", () => {
  it("classifies 24.999% as 1st Year without rounding up to 25", () => {
    // 24.999 units of a 100-unit curriculum.
    const rows = [
      { curriculumCourseId: "a", curriculumUnits: 24.999, status: "PASSED" as const, countsTowardProgramUnits: true },
      { curriculumCourseId: "b", curriculumUnits: 75.001, status: "NOT_TAKEN" as const, countsTowardProgramUnits: true },
    ];
    const result = computeOccp(100, rows);
    expect(result.rawPercentage).toBeCloseTo(24.999, 6);
    // Displayed value rounds to 25.00, but classification must not.
    expect(result.displayPercentage).toBe(25);
    expect(result.yearLevel).toBe("1st Year");
  });

  it("classifies exactly 25% as 2nd Year", () => {
    const result = computeOccp(100, [
      { curriculumCourseId: "a", curriculumUnits: 25, status: "PASSED", countsTowardProgramUnits: true },
      { curriculumCourseId: "b", curriculumUnits: 75, status: "NOT_TAKEN", countsTowardProgramUnits: true },
    ]);
    expect(result.yearLevel).toBe("2nd Year");
  });

  it("classifies 49.999% as 2nd Year and exactly 50% as 3rd Year", () => {
    expect(
      computeOccp(100, [
        { curriculumCourseId: "a", curriculumUnits: 49.999, status: "PASSED", countsTowardProgramUnits: true },
        { curriculumCourseId: "b", curriculumUnits: 50.001, status: "NOT_TAKEN", countsTowardProgramUnits: true },
      ]).yearLevel,
    ).toBe("2nd Year");
    expect(
      computeOccp(100, [
        { curriculumCourseId: "a", curriculumUnits: 50, status: "PASSED", countsTowardProgramUnits: true },
        { curriculumCourseId: "b", curriculumUnits: 50, status: "NOT_TAKEN", countsTowardProgramUnits: true },
      ]).yearLevel,
    ).toBe("3rd Year");
  });

  it("classifies 74.999% as 3rd Year and exactly 75% as 4th Year", () => {
    expect(
      computeOccp(100, [
        { curriculumCourseId: "a", curriculumUnits: 74.999, status: "PASSED", countsTowardProgramUnits: true },
        { curriculumCourseId: "b", curriculumUnits: 25.001, status: "NOT_TAKEN", countsTowardProgramUnits: true },
      ]).yearLevel,
    ).toBe("3rd Year");
    expect(
      computeOccp(100, [
        { curriculumCourseId: "a", curriculumUnits: 75, status: "PASSED", countsTowardProgramUnits: true },
        { curriculumCourseId: "b", curriculumUnits: 25, status: "NOT_TAKEN", countsTowardProgramUnits: true },
      ]).yearLevel,
    ).toBe("4th Year");
  });
});

describe("computeOccp — crediting rules (SPEC 5)", () => {
  it("throws when the curriculum has zero total units", () => {
    expect(() => computeOccp(0, [])).toThrow(OccpCalculationError);
    try {
      computeOccp(0, []);
    } catch (error) {
      expect((error as OccpCalculationError).code).toBe("INVALID_TOTAL_PROGRAM_UNITS");
    }
  });

  it("throws on a negative denominator", () => {
    expect(() => computeOccp(-10, [])).toThrow(OccpCalculationError);
  });

  it("does not double count duplicate assessment rows for the same requirement", () => {
    const duplicated: AssessmentRow[] = [
      { curriculumCourseId: "same", curriculumUnits: 10, status: "PASSED", countsTowardProgramUnits: true },
      { curriculumCourseId: "same", curriculumUnits: 10, status: "PASSED", countsTowardProgramUnits: true },
      { curriculumCourseId: "same", curriculumUnits: 10, status: "PASSED", countsTowardProgramUnits: true },
      { curriculumCourseId: "other", curriculumUnits: 10, status: "NOT_TAKEN", countsTowardProgramUnits: true },
    ];
    const result = computeOccp(20, duplicated);
    expect(result.creditedUnits).toBe(10);
    expect(result.displayPercentage).toBe(50);
  });

  it("counts zero units for a failed course", () => {
    const result = computeOccp(TOTAL, mark(BASE(), 10, "FAILED"));
    expect(result.creditedUnits).toBe(0);
    expect(result.yearLevel).toBe("1st Year");
  });

  it("counts zero units for an in-progress course", () => {
    const result = computeOccp(TOTAL, mark(BASE(), 10, "IN_PROGRESS"));
    expect(result.creditedUnits).toBe(0);
  });

  it("counts zero units for NOT_TAKEN and FOR_VALIDATION", () => {
    expect(computeOccp(TOTAL, mark(BASE(), 10, "NOT_TAKEN")).creditedUnits).toBe(0);
    expect(computeOccp(TOTAL, mark(BASE(), 10, "FOR_VALIDATION")).creditedUnits).toBe(0);
  });

  it("counts the full curriculum requirement units for PASSED and CREDITED", () => {
    expect(computeOccp(TOTAL, mark(BASE(), 5, "PASSED")).creditedUnits).toBe(10);
    expect(computeOccp(TOTAL, mark(BASE(), 5, "CREDITED")).creditedUnits).toBe(10);
  });

  it("ignores requirements flagged as not counting toward program units", () => {
    const rows: AssessmentRow[] = [
      { curriculumCourseId: "counts", curriculumUnits: 50, status: "PASSED", countsTowardProgramUnits: true },
      { curriculumCourseId: "excluded", curriculumUnits: 50, status: "PASSED", countsTowardProgramUnits: false },
      { curriculumCourseId: "rest", curriculumUnits: 50, status: "NOT_TAKEN", countsTowardProgramUnits: true },
    ];
    // Denominator is the caller's business; here only the two counting rows.
    const result = computeOccp(100, rows);
    expect(result.creditedUnits).toBe(50);
    expect(result.displayPercentage).toBe(50);
  });

  it("an extra non-mapped course does not increase the percentage", () => {
    const base = mark(BASE(), 50, "PASSED");
    const before = computeOccp(TOTAL, base);
    // An "extra" course is simply not part of the curriculum row set at all.
    const after = computeOccp(TOTAL, base);
    expect(after.displayPercentage).toBe(before.displayPercentage);
    expect(after.creditedUnits).toBe(100);
  });

  it("caps an approved equivalency override at the requirement's own units", () => {
    const result = computeOccp(100, [
      {
        curriculumCourseId: "a",
        curriculumUnits: 10,
        status: "CREDITED",
        countsTowardProgramUnits: true,
        approvedCreditedUnitsOverride: 999,
      },
      { curriculumCourseId: "b", curriculumUnits: 90, status: "NOT_TAKEN", countsTowardProgramUnits: true },
    ]);
    expect(result.creditedUnits).toBe(10);
  });

  it("honours a reduced approved equivalency override", () => {
    const result = computeOccp(100, [
      {
        curriculumCourseId: "a",
        curriculumUnits: 10,
        status: "CREDITED",
        countsTowardProgramUnits: true,
        approvedCreditedUnitsOverride: 4,
      },
      { curriculumCourseId: "b", curriculumUnits: 90, status: "NOT_TAKEN", countsTowardProgramUnits: true },
    ]);
    expect(result.creditedUnits).toBe(4);
  });

  it("throws when credited units would exceed total program units", () => {
    const rows: AssessmentRow[] = [
      { curriculumCourseId: "a", curriculumUnits: 80, status: "PASSED", countsTowardProgramUnits: true },
      { curriculumCourseId: "b", curriculumUnits: 80, status: "PASSED", countsTowardProgramUnits: true },
    ];
    expect(() => computeOccp(100, rows)).toThrow(/exceed total program units/i);
  });

  it("reports remaining units and remaining requirements", () => {
    const result = computeOccp(TOTAL, mark(BASE(), 30, "PASSED"));
    expect(result.creditedUnits).toBe(60);
    expect(result.remainingUnits).toBe(140);
    expect(result.remainingCourseIds).toHaveLength(70);
    expect(result.countedCourseCount).toBe(30);
    expect(result.totalCourseCount).toBe(100);
  });
});

describe("year-long subjects — ON_FINAL_PASS (SPEC 52.6)", () => {
  const pair = (statusA: AssessmentStatus, statusB: AssessmentStatus): AssessmentRow[] => [
    {
      curriculumCourseId: "A",
      curriculumUnits: 8,
      status: statusA,
      countsTowardProgramUnits: true,
      yearLongGroupId: "YL-1",
      isFinalComponent: false,
    },
    {
      curriculumCourseId: "B",
      curriculumUnits: 8,
      status: statusB,
      countsTowardProgramUnits: true,
      yearLongGroupId: "YL-1",
      isFinalComponent: true,
    },
    { curriculumCourseId: "C", curriculumUnits: 4, status: "NOT_TAKEN", countsTowardProgramUnits: true },
  ];

  it("withholds credit while only the first-semester component is passed", () => {
    const result = computeOccp(20, pair("PASSED", "IN_PROGRESS"));
    expect(result.creditedUnits).toBe(0);
  });

  it("releases both components once the final component is passed", () => {
    const result = computeOccp(20, pair("PASSED", "PASSED"));
    expect(result.creditedUnits).toBe(16);
    expect(result.creditedCourseIds.sort()).toEqual(["A", "B"]);
  });

  it("releases both components when the final component is CREDITED", () => {
    expect(computeOccp(20, pair("IN_PROGRESS", "CREDITED")).creditedUnits).toBe(16);
  });

  it("never counts a component twice", () => {
    const rows = [...pair("PASSED", "PASSED"), ...pair("PASSED", "PASSED")];
    expect(computeOccp(20, rows).creditedUnits).toBe(16);
  });

  it("PER_COMPONENT credits each semester independently", () => {
    const result = computeOccp(20, pair("PASSED", "IN_PROGRESS"), { yearLongCreditPolicy: "PER_COMPONENT" });
    expect(result.creditedUnits).toBe(8);
  });

  it("falls back to per-component when no final component is flagged", () => {
    const rows: AssessmentRow[] = [
      {
        curriculumCourseId: "A",
        curriculumUnits: 8,
        status: "PASSED",
        countsTowardProgramUnits: true,
        yearLongGroupId: "YL-X",
        isFinalComponent: false,
      },
      {
        curriculumCourseId: "B",
        curriculumUnits: 8,
        status: "NOT_TAKEN",
        countsTowardProgramUnits: true,
        yearLongGroupId: "YL-X",
        isFinalComponent: false,
      },
    ];
    expect(computeOccp(16, rows).creditedUnits).toBe(8);
  });
});

describe("display rounding and helpers", () => {
  it("rounds display to two decimals by default", () => {
    expect(roundForDisplay(66.66666666)).toBe(66.67);
    expect(roundForDisplay(33.333333)).toBe(33.33);
  });

  it("honours a configured precision", () => {
    expect(roundForDisplay(66.66666666, 4)).toBe(66.6667);
    expect(roundForDisplay(66.66666666, 0)).toBe(67);
  });

  it("computes unit boundaries for the 237-unit approved curriculum (SPEC 52.12)", () => {
    const boundaries = yearLevelUnitBoundaries(237);
    expect(boundaries.secondYear).toBe(59.25);
    expect(boundaries.thirdYear).toBe(118.5);
    expect(boundaries.fourthYear).toBe(177.75);
  });

  it("computeOccpSafe returns an error object instead of throwing", () => {
    const result = computeOccpSafe(0, []);
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.code).toBe("INVALID_TOTAL_PROGRAM_UNITS");
  });
});

describe("the 237-unit approved curriculum end to end", () => {
  it("produces the documented year-level unit boundaries", () => {
    const rows = curriculum(237, 1);
    expect(computeOccp(237, mark(rows, 59, "PASSED")).yearLevel).toBe("1st Year");
    expect(computeOccp(237, mark(rows, 60, "PASSED")).yearLevel).toBe("2nd Year");
    expect(computeOccp(237, mark(rows, 118, "PASSED")).yearLevel).toBe("2nd Year");
    expect(computeOccp(237, mark(rows, 119, "PASSED")).yearLevel).toBe("3rd Year");
    expect(computeOccp(237, mark(rows, 177, "PASSED")).yearLevel).toBe("3rd Year");
    expect(computeOccp(237, mark(rows, 178, "PASSED")).yearLevel).toBe("4th Year");
    expect(computeOccp(237, mark(rows, 237, "PASSED")).displayPercentage).toBe(100);
  });
});
