import { describe, expect, it } from "vitest";
import { detectMultiEntry, assignmentToPermutation, isPermutation } from "./multiEntry";

// field -> source header, the shape stored on the upload. property_type and
// built_form_type intentionally share a header (the classifier allows it).
const MAPPING = {
  address_1: "Addr",
  postcode: "PC",
  property_type: "Property Type",
  wall_type: "Walls",
  roof_type: "Roofs",
};

describe("detectMultiEntry", () => {
  it("returns an empty summary when no classifier columns are mapped", () => {
    const rows = [{ Addr: "1 High St", PC: "AB1 2CD" }];
    const summary = detectMultiEntry(rows, { address_1: "Addr", postcode: "PC" });
    expect(summary.sample).toBeNull();
    expect(summary.largestCount).toBe(0);
  });

  it("captures a single-part verify sample when classifier columns exist but no row is multi-entry", () => {
    const rows = [
      { Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: EndTerrace", Walls: "Cavity: AsBuilt", Roofs: "Pitched: 200mm" },
    ];
    const summary = detectMultiEntry(rows, MAPPING);

    // Not multi-entry, but there IS a sample to verify (ADR-0004 Step 1).
    expect(summary.largestCount).toBe(0);
    expect(summary.sample).not.toBeNull();
    expect(summary.sample!.count).toBe(1);
    expect(summary.sample!.address).toBe("1 High St, AB1 2CD");
    // All three mapped classifier columns are present, one entry each.
    expect(summary.sample!.columns.map((c) => c.field).sort()).toEqual([
      "property_type",
      "roof_type",
      "wall_type",
    ]);
    expect(summary.sample!.columns.every((c) => c.entries.length === 1)).toBe(true);
  });

  it("picks the largest-count row as the sample and reports it as multi-entry", () => {
    const rows = [
      { Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: EndTerrace", Walls: "Cavity: AsBuilt", Roofs: "Pitched: 200mm" },
      { Addr: "2 Low St", PC: "AB3 4EF", "Property Type": "House: Detached", Walls: "Cavity: AsBuilt, Cavity: Filled", Roofs: "Flat: AsBuilt, Pitched: 200mm" },
    ];
    const summary = detectMultiEntry(rows, MAPPING);

    expect(summary.largestCount).toBe(2);
    expect(summary.countDistribution).toEqual({ "2": 1 });
    expect(summary.sample!.address).toBe("2 Low St, AB3 4EF");
    expect(summary.sample!.count).toBe(2);
    // multiValuedFields are the ones that actually split.
    expect([...summary.multiValuedFields].sort()).toEqual(["roof_type", "wall_type"]);
    // The whole-dwelling Property Type column is still carried (for Step 1),
    // with a single entry — Step 2 filters it out by entries.length.
    const propertyCol = summary.sample!.columns.find((c) => c.field === "property_type");
    expect(propertyCol?.entries).toHaveLength(1);
    const wallCol = summary.sample!.columns.find((c) => c.field === "wall_type");
    expect(wallCol?.entries.map((e) => e.raw)).toEqual(["Cavity: AsBuilt", "Cavity: Filled"]);
  });

  it("captures one ordering sample per distinct count (ADR-0004 amendment)", () => {
    const rows = [
      { Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: Detached", Walls: "Cavity: AsBuilt", Roofs: "Pitched: 200mm" }, // count 1
      { Addr: "2 Low St", PC: "AB3 4EF", "Property Type": "House: Semi", Walls: "Cavity, Solid", Roofs: "Flat, Pitched" }, // count 2
      { Addr: "3 Mid Rd", PC: "AB5 6GH", "Property Type": "House: Mid", Walls: "Cavity, Solid, Render", Roofs: "Flat, Pitched, Slate" }, // count 3
      { Addr: "4 Side Ln", PC: "AB7 8IJ", "Property Type": "House: Other", Walls: "Brick, Stone", Roofs: "Tile, Slate" }, // count 2 again
    ];
    const summary = detectMultiEntry(rows, MAPPING);

    expect(summary.largestCount).toBe(3);
    expect(summary.countDistribution).toEqual({ "2": 2, "3": 1 });

    // A sample for every count >= 2 — and only those.
    expect(Object.keys(summary.samplesByCount ?? {}).sort()).toEqual(["2", "3"]);
    expect(summary.samplesByCount!["2"].count).toBe(2);
    expect(summary.samplesByCount!["3"].count).toBe(3);
    // The count-2 sample is the FIRST count-2 row, not the count-3 one.
    expect(summary.samplesByCount!["2"].address).toBe("2 Low St, AB3 4EF");
    const wall2 = summary.samplesByCount!["2"].columns.find((c) => c.field === "wall_type");
    expect(wall2?.entries.map((e) => e.raw)).toEqual(["Cavity", "Solid"]);
    const wall3 = summary.samplesByCount!["3"].columns.find((c) => c.field === "wall_type");
    expect(wall3?.entries.map((e) => e.raw)).toEqual(["Cavity", "Solid", "Render"]);
  });

  it("normalizes descriptions to lower-case (matching the classifier's key)", () => {
    const rows = [{ Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: EndTerrace", Walls: "", Roofs: "" }];
    const summary = detectMultiEntry(rows, MAPPING);
    const entry = summary.sample!.columns.find((c) => c.field === "property_type")!.entries[0];
    expect(entry.raw).toBe("House: EndTerrace");
    expect(entry.description).toBe("house: endterrace");
  });
});

describe("ordering helpers", () => {
  it("isPermutation accepts a bijection and rejects duplicates/out-of-range", () => {
    expect(isPermutation([0, 1])).toBe(true);
    expect(isPermutation([1, 0, 2])).toBe(true);
    expect(isPermutation([0, 0])).toBe(false);
    expect(isPermutation([0, 2])).toBe(false);
  });

  it("assignmentToPermutation inverts assignment[pos]=slot to permutation[slot]=pos", () => {
    // file position 1 holds the main building (slot 0), position 0 is extension 1.
    expect(assignmentToPermutation([1, 0])).toEqual([1, 0]);
    expect(assignmentToPermutation([0, 1, 2])).toEqual([0, 1, 2]);
    expect(assignmentToPermutation([2, 0, 1])).toEqual([1, 2, 0]);
  });
});
