import { describe, expect, it } from "vitest";
import { parseCsv, suggestMapping, assertAcceptedFile, WorkbookParseError } from "@/lib/imports/workbook";
import {
  buildCurriculumPreview,
  CURRICULUM_COLUMNS,
  parseAcademicYear,
  parseTerm,
} from "@/lib/imports/curriculum-import";
import { buildStudentPreview, STUDENT_COLUMNS } from "@/lib/imports/student-import";
import {
  ASSESSMENT_COLUMNS,
  buildAssessmentPreview,
  parseStatusCell,
} from "@/lib/imports/assessment-import";
import type { ParsedSheet } from "@/lib/imports/workbook";
import { normalizeCourseCode } from "@/lib/curriculum-resolver";
import type { CurriculumAssignmentRule } from "@/types";

/** Import validation tests (SPEC 40). */

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

/** Turns a CSV block into the ParsedSheet shape the previewers expect. */
function sheetFromCsv(name: string, csv: string): ParsedSheet {
  const matrix = parseCsv(csv.trim());
  const headers = matrix[0] ?? [];
  const rows = matrix.slice(1).filter((row) => row.some((cell) => cell !== ""));
  return { name, headers, rows, headerRowNumber: 1, totalRowCount: rows.length };
}

function mappingFor(sheet: ParsedSheet, definitions: typeof CURRICULUM_COLUMNS) {
  return suggestMapping(sheet.headers, [...definitions]);
}

function rule(overrides: Partial<CurriculumAssignmentRule> & { name: string; curriculum_id: string }): CurriculumAssignmentRule {
  return {
    id: `rule-${overrides.name}`,
    name: overrides.name,
    curriculum_id: overrides.curriculum_id,
    rule_type: overrides.rule_type ?? "PREFIX",
    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: null,
    created_at: "2026-01-01T00:00:00.000Z",
    updated_at: "2026-01-01T00:00:00.000Z",
  };
}

// -----------------------------------------------------------------------------
// File acceptance
// -----------------------------------------------------------------------------

describe("file acceptance", () => {
  it("accepts .xlsx and .csv within the size limit", () => {
    expect(() => assertAcceptedFile("students.xlsx", 1024, 10)).not.toThrow();
    expect(() => assertAcceptedFile("students.csv", 1024, 10)).not.toThrow();
  });

  it("rejects legacy .xls with an actionable message", () => {
    expect(() => assertAcceptedFile("old.xls", 1024, 10)).toThrow(/save it as \.xlsx/i);
  });

  it("rejects unsupported extensions", () => {
    try {
      assertAcceptedFile("notes.pdf", 1024, 10);
      throw new Error("should have thrown");
    } catch (error) {
      expect((error as WorkbookParseError).code).toBe("UNSUPPORTED_FILE_TYPE");
    }
  });

  it("rejects a file above the configured size limit", () => {
    expect(() => assertAcceptedFile("big.xlsx", 11 * 1024 * 1024, 10)).toThrow(/exceeds the 10 MB import limit/);
  });

  it("rejects an empty file", () => {
    expect(() => assertAcceptedFile("empty.xlsx", 0, 10)).toThrow(/empty/i);
  });
});

describe("parseCsv", () => {
  it("handles quoted fields, embedded commas and CRLF", () => {
    const rows = parseCsv('a,b\r\n"x,1","y ""q"""\r\n');
    expect(rows).toEqual([
      ["a", "b"],
      ["x,1", 'y "q"'],
    ]);
  });

  it("strips a UTF-8 BOM", () => {
    expect(parseCsv("﻿a,b\n1,2")[0]).toEqual(["a", "b"]);
  });
});

describe("suggestMapping", () => {
  it("matches headers regardless of spacing, case and punctuation", () => {
    const mapping = suggestMapping(
      ["Student No.", "SURNAME", "given name", "Year Graduated"],
      [...STUDENT_COLUMNS],
    );
    expect(mapping.student_number).toBe(0);
    expect(mapping.last_name).toBe(1);
    expect(mapping.first_name).toBe(2);
    expect(mapping.graduation_year).toBe(3);
  });

  it("leaves unmatched fields null rather than guessing", () => {
    const mapping = suggestMapping(["Student No."], [...STUDENT_COLUMNS]);
    expect(mapping.student_number).toBe(0);
    expect(mapping.gwa).toBeNull();
  });

  it("never maps two fields onto the same column", () => {
    const mapping = suggestMapping(["Course Code", "Course Title", "Units"], [...CURRICULUM_COLUMNS]);
    const used = Object.values(mapping).filter((v): v is number => v !== null);
    expect(new Set(used).size).toBe(used.length);
  });
});

// -----------------------------------------------------------------------------
// Academic year / term parsing
// -----------------------------------------------------------------------------

describe("parseAcademicYear", () => {
  it.each([
    ["1", 1],
    ["1st Year", 1],
    ["FIRST YEAR", 1],
    ["Year 2", 2],
    ["SECOND YEAR - 1ST SEMESTER", 2],
    ["III", 3],
    ["", null],
    ["not a year", null],
  ])("%s => %s", (input, expected) => {
    expect(parseAcademicYear(input as string)).toBe(expected);
  });
});

describe("parseTerm", () => {
  it.each([
    ["1st sem", "1ST SEMESTER"],
    ["Semester 2", "2ND SEMESTER"],
    ["SECOND SEMESTER", "2ND SEMESTER"],
    ["Summer", "SUMMER"],
    ["", ""],
  ])("%s => %s", (input, expected) => {
    expect(parseTerm(input as string)).toBe(expected);
  });
});

// -----------------------------------------------------------------------------
// Curriculum import
// -----------------------------------------------------------------------------

describe("curriculum import preview", () => {
  const validCsv = `
Course Code,Subject Description,Lec,Lab,Total Units,Hours/Week,Year,Semester,Prerequisite
HSB 1-AG,Normal Organ Systems (Structure-Gross),3,5,8,10,FIRST YEAR,1ST SEMESTER,Any B.S. Course
PH1,Public Health I,2,0,2,2,FIRST YEAR,1ST SEMESTER,Any B.S. Course
HSB 1-BG,Normal Organ Systems (Structure-Gross),3,5,8,10,FIRST YEAR,2ND SEMESTER,HSB 1-AG
`;

  it("accepts a clean file and derives the total units", () => {
    const sheet = sheetFromCsv("CURRICULUM", validCsv);
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    expect(preview.detectedRowCount).toBe(3);
    expect(preview.validRowCount).toBe(3);
    expect(preview.totalUnits).toBe(18);
    expect(preview.errorCount).toBe(0);
    expect(preview.canCommit).toBe(true);
  });

  it("groups units by academic year and term", () => {
    const sheet = sheetFromCsv("CURRICULUM", validCsv);
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    expect(preview.unitsByYearTerm).toEqual([
      { intended_year: 1, term: "1ST SEMESTER", count: 2, units: 10 },
      { intended_year: 1, term: "2ND SEMESTER", count: 1, units: 8 },
    ]);
  });

  it("blocks a blank course code", () => {
    const sheet = sheetFromCsv(
      "S",
      `
Course Code,Subject Description,Total Units,Year,Semester
,Missing code,3,1,1ST SEMESTER
`,
    );
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    expect(preview.issues.some((i) => i.code === "BLANK_COURSE_CODE")).toBe(true);
    expect(preview.canCommit).toBe(false);
  });

  it("blocks blank units and invalid units separately", () => {
    const sheet = sheetFromCsv(
      "S",
      `
Course Code,Subject Description,Total Units,Year,Semester
A-1,Blank units,,1,1ST SEMESTER
A-2,Bad units,three,1,1ST SEMESTER
A-3,Negative units,-4,1,1ST SEMESTER
`,
    );
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    const codes = preview.issues.map((i) => i.code);
    expect(codes).toContain("BLANK_UNITS");
    expect(codes).toContain("INVALID_UNITS");
    expect(codes).toContain("NEGATIVE_UNITS");
    expect(preview.canCommit).toBe(false);
  });

  it("blocks a duplicate course code within the same year and term", () => {
    const sheet = sheetFromCsv(
      "S",
      `
Course Code,Subject Description,Total Units,Year,Semester
DUP-1,First,3,1,1ST SEMESTER
dup 1,Second,3,1,1ST SEMESTER
`,
    );
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    expect(preview.issues.some((i) => i.code === "DUPLICATE_COURSE_CODE")).toBe(true);
    expect(preview.canCommit).toBe(false);
  });

  it("allows the same course code in a different academic period", () => {
    const sheet = sheetFromCsv(
      "S",
      `
Course Code,Subject Description,Total Units,Year,Semester
SAME-1,First semester,3,1,1ST SEMESTER
SAME-1,Second semester,3,1,2ND SEMESTER
`,
    );
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    expect(preview.issues.some((i) => i.code === "DUPLICATE_COURSE_CODE")).toBe(false);
    expect(preview.canCommit).toBe(true);
  });

  it("blocks a lecture + laboratory total that does not equal total units", () => {
    const sheet = sheetFromCsv(
      "S",
      `
Course Code,Subject Description,Lec,Lab,Total Units,Year,Semester
BAD-1,Mismatched,3,3,8,1,1ST SEMESTER
`,
    );
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    expect(preview.issues.some((i) => i.code === "LEC_LAB_UNIT_MISMATCH")).toBe(true);
    expect(preview.canCommit).toBe(false);
  });

  it("reports an unmapped required column instead of silently importing", () => {
    const sheet = sheetFromCsv("S", `Subject Description,Total Units\nOnly a title,3`);
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    expect(preview.issues.some((i) => i.code === "MISSING_COLUMN_MAPPING")).toBe(true);
    expect(preview.canCommit).toBe(false);
  });

  it("excludes rows flagged as not counting toward program units from the total", () => {
    const sheet = sheetFromCsv(
      "S",
      `
Course Code,Subject Description,Total Units,Year,Semester,Counts Toward Program Units
IN-1,Counts,10,1,1ST SEMESTER,Yes
OUT-1,Excluded,10,1,1ST SEMESTER,No
`,
    );
    const preview = buildCurriculumPreview(sheet, mappingFor(sheet, CURRICULUM_COLUMNS));
    expect(preview.courseCount).toBe(2);
    expect(preview.totalUnits).toBe(10);
  });
});

// -----------------------------------------------------------------------------
// Student import
// -----------------------------------------------------------------------------

describe("student import preview", () => {
  const rules = [rule({ name: "2024 prefix", curriculum_id: "curr-2024", prefix: "2024" })];
  const curriculaByCode = new Map([["CURR-2023", { id: "curr-2023", code: "CURR-2023" }]]);
  const curriculaById = new Map([
    ["curr-2023", { id: "curr-2023", code: "CURR-2023" }],
    ["curr-2024", { id: "curr-2024", code: "CURR-2024" }],
  ]);

  const base = (csv: string, options: Partial<Parameters<typeof buildStudentPreview>[0]> = {}) => {
    const sheet = sheetFromCsv("Sheet1", csv);
    return buildStudentPreview({
      sheet,
      mapping: suggestMapping(sheet.headers, [...STUDENT_COLUMNS]),
      mode: "CREATE_ONLY",
      rules,
      normalization: "UPPERCASE_STRIP_SPACES_HYPHENS",
      existing: new Map(),
      curriculaByCode,
      curriculaById,
      ...options,
    });
  };

  it("accepts a valid file and auto-detects the curriculum", () => {
    const preview = base(`
Student Number,Last Name,First Name,Year Graduated,NMAT Score
2024-0001,Reyes,Ana,2023,88
`);
    expect(preview.createCount).toBe(1);
    expect(preview.rejectCount).toBe(0);
    expect(preview.rows[0].resolvedCurriculumCode).toBe("CURR-2024");
    expect(preview.rows[0].assignmentMethod).toBe("AUTO");
    expect(preview.canCommit).toBe(true);
  });

  it("rejects a missing student number", () => {
    const preview = base(`
Student Number,Last Name,First Name
,Reyes,Ana
`);
    expect(preview.rejectCount).toBe(1);
    expect(preview.rows[0].errors.join(" ")).toMatch(/Student number is required/);
  });

  it("rejects duplicate student numbers within the file", () => {
    const preview = base(`
Student Number,Last Name,First Name
2024-0001,Reyes,Ana
2024 0001,Reyes,Ana
`);
    expect(preview.duplicateInFileCount).toBe(1);
    expect(preview.rejectCount).toBe(1);
    expect(preview.createCount).toBe(1);
  });

  it("skips an existing student in CREATE_ONLY mode", () => {
    const preview = base(
      `
Student Number,Last Name,First Name
2024-0001,Reyes,Ana
`,
      { existing: new Map([["20240001", { id: "s1", last_name: "Reyes", first_name: "Ana" }]]) },
    );
    expect(preview.skipCount).toBe(1);
    expect(preview.createCount).toBe(0);
    expect(preview.canCommit).toBe(false);
  });

  it("lists changed fields in UPDATE_EXISTING mode", () => {
    const preview = base(
      `
Student Number,Last Name,First Name,GWA
2024-0001,Reyes,Anna,1.50
`,
      {
        mode: "UPDATE_EXISTING_BY_STUDENT_NUMBER",
        existing: new Map([
          ["20240001", { id: "s1", last_name: "Reyes", first_name: "Ana", gwa: "1.75" }],
        ]),
      },
    );
    expect(preview.updateCount).toBe(1);
    expect(preview.rows[0].changedFields).toContain("first_name");
    expect(preview.rows[0].changedFields).toContain("gwa");
    expect(preview.rows[0].changedFields).not.toContain("last_name");
  });

  it("flags an unresolved curriculum as a warning, not a rejection", () => {
    const preview = base(`
Student Number,Last Name,First Name
TRF-9001,Ocampo,Bea
`);
    expect(preview.createCount).toBe(1);
    expect(preview.unresolvedCount).toBe(1);
    expect(preview.rows[0].resolvedCurriculumId).toBeNull();
    expect(preview.rows[0].warnings.join(" ")).toMatch(/assigned manually/i);
  });

  it("reports a rule conflict without guessing a curriculum", () => {
    const preview = base(
      `
Student Number,Last Name,First Name
2025-0001,Cruz,Leo
`,
      {
        rules: [
          rule({ name: "A", curriculum_id: "curr-x", prefix: "2025", priority: 100 }),
          rule({ name: "B", curriculum_id: "curr-y", prefix: "2025", priority: 100 }),
        ],
      },
    );
    expect(preview.conflictCount).toBe(1);
    expect(preview.rows[0].resolvedCurriculumId).toBeNull();
  });

  it("honours an explicit curriculum column as a manual assignment", () => {
    const preview = base(`
Student Number,Last Name,First Name,Curriculum
2024-0001,Reyes,Ana,CURR-2023
`);
    expect(preview.rows[0].assignmentMethod).toBe("MANUAL");
    expect(preview.rows[0].resolvedCurriculumCode).toBe("CURR-2023");
  });

  it("rejects an unknown explicit curriculum code", () => {
    const preview = base(`
Student Number,Last Name,First Name,Curriculum
2024-0001,Reyes,Ana,CURR-9999
`);
    expect(preview.rejectCount).toBe(1);
    expect(preview.rows[0].errors.join(" ")).toMatch(/does not exist/);
  });

  it("rejects invalid years and out-of-range NMAT scores", () => {
    const preview = base(`
Student Number,Last Name,First Name,Year Graduated,NMAT Score
2024-0001,Reyes,Ana,20XX,88
2024-0002,Cruz,Ben,2023,500
`);
    expect(preview.rejectCount).toBe(2);
    expect(preview.rows[0].errors.join(" ")).toMatch(/valid 4-digit year/);
    expect(preview.rows[1].errors.join(" ")).toMatch(/between 0 and 100/);
  });

  it("rejects the whole import when a required column is unmapped", () => {
    const sheet = sheetFromCsv("Sheet1", `Last Name,First Name\nReyes,Ana`);
    const preview = buildStudentPreview({
      sheet,
      mapping: suggestMapping(sheet.headers, [...STUDENT_COLUMNS]),
      mode: "CREATE_ONLY",
      rules,
      normalization: "UPPERCASE_STRIP_SPACES_HYPHENS",
      existing: new Map(),
      curriculaByCode,
      curriculaById,
    });
    expect(preview.rows.some((r) => r.errors.some((e) => /has not been mapped/.test(e)))).toBe(true);
  });
});

// -----------------------------------------------------------------------------
// Assessment import
// -----------------------------------------------------------------------------

describe("parseStatusCell", () => {
  it("maps recognised synonyms onto controlled statuses", () => {
    expect(parseStatusCell("pass")).toEqual({ status: "PASSED", recognised: true });
    expect(parseStatusCell("Credited")).toEqual({ status: "CREDITED", recognised: true });
    expect(parseStatusCell("in progress")).toEqual({ status: "IN_PROGRESS", recognised: true });
    expect(parseStatusCell("FAIL")).toEqual({ status: "FAILED", recognised: true });
  });

  it("routes an unrecognised or blank status to FOR_VALIDATION rather than guessing", () => {
    expect(parseStatusCell("¿?")).toEqual({ status: "FOR_VALIDATION", recognised: false });
    expect(parseStatusCell("")).toEqual({ status: "FOR_VALIDATION", recognised: false });
  });
});

describe("assessment import preview", () => {
  const students = new Map([
    ["20240001", { id: "s1", name: "Reyes, Ana", curriculum_id: "curr-2024" }],
    ["20240002", { id: "s2", name: "Cruz, Ben", curriculum_id: null }],
  ]);
  // Keyed exactly as the server builds it: `${curriculumId}::${normalizeCourseCode(code)}`.
  const courses = new Map([
    [`curr-2024::${normalizeCourseCode("MED 101")}`, { id: "c1", title: "Sample Course", units: 6 }],
    [`curr-2024::${normalizeCourseCode("MED 102")}`, { id: "c2", title: "Another Course", units: 4 }],
  ]);

  const base = (csv: string) => {
    const sheet = sheetFromCsv("Sheet1", csv);
    return buildAssessmentPreview({
      sheet,
      mapping: suggestMapping(sheet.headers, [...ASSESSMENT_COLUMNS]),
      normalization: "UPPERCASE_STRIP_SPACES_HYPHENS",
      students,
      courses,
    });
  };

  it("applies a recognised passing status", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Grade,Status
2024-0001,MED 101,85,PASSED
`);
    expect(preview.applyCount).toBe(1);
    expect(preview.rows[0].status).toBe("PASSED");
    expect(preview.rows[0].curriculumCourseId).toBe("c1");
  });

  it("rejects a student that does not exist", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Grade,Status
9999-9999,MED 101,85,PASSED
`);
    expect(preview.rejectCount).toBe(1);
    expect(preview.rows[0].errors.join(" ")).toMatch(/No student exists/);
  });

  it("rejects a student with no assigned curriculum", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Grade,Status
2024-0002,MED 101,85,PASSED
`);
    expect(preview.rejectCount).toBe(1);
    expect(preview.rows[0].errors.join(" ")).toMatch(/no assigned curriculum/i);
  });

  it("rejects a course that is not in the student's assigned curriculum", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Grade,Status
2024-0001,NOT-IN-CURRICULUM,85,PASSED
`);
    expect(preview.rejectCount).toBe(1);
    expect(preview.rows[0].errors.join(" ")).toMatch(/does not exist in the student's assigned curriculum/);
  });

  it("routes an unknown status to FOR_VALIDATION with a warning", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Grade,Status
2024-0001,MED 101,85,completed with honours
`);
    expect(preview.validationCount).toBe(1);
    expect(preview.rows[0].status).toBe("FOR_VALIDATION");
    expect(preview.rows[0].warnings.join(" ")).toMatch(/not recognised/);
  });

  it("does not infer credit from a grade when no status is supplied", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Grade,Status
2024-0001,MED 101,95,
`);
    expect(preview.rows[0].status).toBe("FOR_VALIDATION");
    expect(preview.rows[0].warnings.join(" ")).toMatch(/rather than inferring credit/);
  });

  it("caps a credited-units override at the requirement's own units", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Status,Credited Units
2024-0001,MED 101,CREDITED,99
`);
    expect(preview.rows[0].creditedUnitsOverride).toBe(6);
    expect(preview.rows[0].warnings.join(" ")).toMatch(/capped at 6/);
  });

  it("rejects a duplicate student/course pair", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Status
2024-0001,MED 101,PASSED
2024-0001,MED 101,FAILED
`);
    expect(preview.rejectCount).toBe(1);
    expect(preview.rows[1].errors.join(" ")).toMatch(/Duplicate row/);
  });

  it("matches course codes case-insensitively and ignores extra spacing", () => {
    const preview = base(`
Student Number,Curriculum Course Code,Status
2024-0001,  med   101 ,PASSED
`);
    expect(preview.applyCount).toBe(1);
    expect(preview.rows[0].curriculumCourseId).toBe("c1");
  });
});
