import { describe, expect, it } from "vitest";
import {
  normalizeCourseCode,
  normalizeStudentNumber,
  resolveCurriculumForStudentNumber,
  ruleMatches,
} from "@/lib/curriculum-resolver";
import type { CurriculumAssignmentRule, RuleType } from "@/types";

function rule(overrides: Partial<CurriculumAssignmentRule> & { name: string; curriculum_id: string; rule_type: RuleType }): CurriculumAssignmentRule {
  return {
    id: overrides.id ?? `rule-${overrides.name}`,
    name: overrides.name,
    curriculum_id: overrides.curriculum_id,
    rule_type: overrides.rule_type,
    prefix: overrides.prefix ?? null,
    regex_pattern: overrides.regex_pattern ?? null,
    exact_student_number: overrides.exact_student_number ?? null,
    numeric_start: overrides.numeric_start ?? null,
    numeric_end: overrides.numeric_end ?? null,
    priority: overrides.priority ?? 100,
    active: overrides.active ?? 1,
    description: overrides.description ?? null,
    created_at: "2026-01-01T00:00:00.000Z",
    updated_at: "2026-01-01T00:00:00.000Z",
  };
}

describe("normalizeStudentNumber", () => {
  it("strips spaces and hyphens and upper-cases by default", () => {
    expect(normalizeStudentNumber(" 2024-0001 ")).toBe("20240001");
    expect(normalizeStudentNumber("md 2024 0001")).toBe("MD20240001");
    expect(normalizeStudentNumber("trf-9001")).toBe("TRF9001");
  });

  it("supports the other configured normalisation modes", () => {
    expect(normalizeStudentNumber("2024-0001", "UPPERCASE_STRIP_SPACES")).toBe("2024-0001");
    expect(normalizeStudentNumber(" 2024-0001 ", "UPPERCASE_TRIM")).toBe("2024-0001");
    expect(normalizeStudentNumber(" md-1 ", "NONE")).toBe("md-1");
  });

  it("handles unicode dashes", () => {
    expect(normalizeStudentNumber("2024‑0001")).toBe("20240001");
  });
});

describe("normalizeCourseCode", () => {
  it("strips punctuation and spacing and upper-cases", () => {
    expect(normalizeCourseCode("  hsb   1-ag ")).toBe("HSB1AG");
    expect(normalizeCourseCode("Biochem 1-A")).toBe("BIOCHEM1A");
  });

  it("collapses inconsistent transcriptions of the same code onto one key", () => {
    expect(normalizeCourseCode("PH1")).toBe(normalizeCourseCode("PH 1"));
    expect(normalizeCourseCode("HSB 1-AG")).toBe(normalizeCourseCode("hsb 1 ag"));
    expect(normalizeCourseCode("Med Juris-LM")).toBe("MEDJURISLM");
  });

  it("still distinguishes genuinely different codes", () => {
    expect(normalizeCourseCode("PH 2-A")).not.toBe(normalizeCourseCode("PH 2-B"));
    expect(normalizeCourseCode("HSB 1-AG")).not.toBe(normalizeCourseCode("HSB 1-AH"));
  });
});

describe("ruleMatches", () => {
  const normalize = (v: string) => normalizeStudentNumber(v);

  it("matches a PREFIX rule", () => {
    const r = rule({ name: "p", curriculum_id: "c1", rule_type: "PREFIX", prefix: "2024" });
    expect(ruleMatches(r, "20240001", normalize)).toBe(true);
    expect(ruleMatches(r, "20230001", normalize)).toBe(false);
  });

  it("matches an EXACT rule after normalisation", () => {
    const r = rule({ name: "e", curriculum_id: "c1", rule_type: "EXACT", exact_student_number: "trf-9001" });
    expect(ruleMatches(r, "TRF9001", normalize)).toBe(true);
    expect(ruleMatches(r, "TRF9002", normalize)).toBe(false);
  });

  it("matches a REGEX rule", () => {
    const r = rule({ name: "r", curriculum_id: "c1", rule_type: "REGEX", regex_pattern: "^2025\\d{4}$" });
    expect(ruleMatches(r, "20250001", normalize)).toBe(true);
    expect(ruleMatches(r, "2025001", normalize)).toBe(false);
  });

  it("never throws on an invalid regular expression", () => {
    const r = rule({ name: "bad", curriculum_id: "c1", rule_type: "REGEX", regex_pattern: "([unclosed" });
    expect(() => ruleMatches(r, "20250001", normalize)).not.toThrow();
    expect(ruleMatches(r, "20250001", normalize)).toBe(false);
  });

  it("matches a NUMERIC_RANGE rule only for purely numeric student numbers", () => {
    const r = rule({
      name: "n",
      curriculum_id: "c1",
      rule_type: "NUMERIC_RANGE",
      numeric_start: 20230000,
      numeric_end: 20239999,
    });
    expect(ruleMatches(r, "20230500", normalize)).toBe(true);
    expect(ruleMatches(r, "20240500", normalize)).toBe(false);
    // Alphanumeric conventions are never coerced into a number.
    expect(ruleMatches(r, "MD20230500", normalize)).toBe(false);
  });

  it("ignores inactive rules", () => {
    const r = rule({ name: "p", curriculum_id: "c1", rule_type: "PREFIX", prefix: "2024", active: 0 });
    expect(ruleMatches(r, "20240001", normalize)).toBe(false);
  });
});

describe("resolveCurriculumForStudentNumber", () => {
  const r2023 = rule({ name: "2023 prefix", curriculum_id: "curr-2023", rule_type: "PREFIX", prefix: "2023" });
  const r2024 = rule({ name: "2024 prefix", curriculum_id: "curr-2024", rule_type: "PREFIX", prefix: "2024" });

  it("auto-assigns when exactly one rule matches", () => {
    const result = resolveCurriculumForStudentNumber("2024-0001", [r2023, r2024]);
    expect(result.outcome).toBe("MATCHED");
    expect(result.curriculumId).toBe("curr-2024");
    expect(result.matchedRule?.name).toBe("2024 prefix");
    expect(result.message).toContain("2024 prefix");
  });

  it("reports UNRESOLVED when no rule matches", () => {
    const result = resolveCurriculumForStudentNumber("TRF-9001", [r2023, r2024]);
    expect(result.outcome).toBe("UNRESOLVED");
    expect(result.curriculumId).toBeNull();
    expect(result.message).toContain("CURRICULUM_UNRESOLVED");
  });

  it("reports UNRESOLVED for an empty student number", () => {
    expect(resolveCurriculumForStudentNumber("   ", [r2023]).outcome).toBe("UNRESOLVED");
  });

  it("reports CONFLICT for equal-priority rules pointing at different curricula", () => {
    const a = rule({ name: "A", curriculum_id: "curr-x", rule_type: "PREFIX", prefix: "2025", priority: 100 });
    const b = rule({ name: "B", curriculum_id: "curr-y", rule_type: "PREFIX", prefix: "2025", priority: 100 });
    const result = resolveCurriculumForStudentNumber("2025-0001", [a, b]);
    expect(result.outcome).toBe("CONFLICT");
    expect(result.curriculumId).toBeNull();
    expect(result.conflictingCurriculumIds.sort()).toEqual(["curr-x", "curr-y"]);
    expect(result.message).toContain("CURRICULUM_RULE_CONFLICT");
  });

  it("is NOT a conflict when equal-priority rules agree on the curriculum", () => {
    const a = rule({ name: "A", curriculum_id: "curr-x", rule_type: "PREFIX", prefix: "2025", priority: 100 });
    const b = rule({ name: "B", curriculum_id: "curr-x", rule_type: "REGEX", regex_pattern: "^2025", priority: 100 });
    const result = resolveCurriculumForStudentNumber("2025-0001", [a, b]);
    expect(result.outcome).toBe("MATCHED");
    expect(result.curriculumId).toBe("curr-x");
    expect(result.matchedRules).toHaveLength(2);
  });

  it("lets the higher-priority rule win", () => {
    const low = rule({ name: "low", curriculum_id: "curr-low", rule_type: "PREFIX", prefix: "2025", priority: 10 });
    const high = rule({ name: "high", curriculum_id: "curr-high", rule_type: "EXACT", exact_student_number: "2025-0001", priority: 900 });
    const result = resolveCurriculumForStudentNumber("2025-0001", [low, high]);
    expect(result.outcome).toBe("MATCHED");
    expect(result.curriculumId).toBe("curr-high");
    expect(result.matchedRule?.name).toBe("high");
  });

  it("skips inactive rules entirely", () => {
    const inactive = rule({ name: "off", curriculum_id: "curr-off", rule_type: "PREFIX", prefix: "2024", active: 0 });
    const result = resolveCurriculumForStudentNumber("2024-0001", [inactive]);
    expect(result.outcome).toBe("UNRESOLVED");
  });

  it("respects a different normalisation mode", () => {
    const hyphenated = rule({ name: "h", curriculum_id: "curr-h", rule_type: "EXACT", exact_student_number: "2024-0001" });
    expect(
      resolveCurriculumForStudentNumber("2024-0001", [hyphenated], { normalization: "UPPERCASE_TRIM" }).outcome,
    ).toBe("MATCHED");
    // Under the default mode both sides lose the hyphen, so it still matches.
    expect(resolveCurriculumForStudentNumber("2024-0001", [hyphenated]).outcome).toBe("MATCHED");
  });
});
