import { describe, expect, it } from "vitest";
import {
  constraintSummary,
  deriveScenarioStatus,
  DISRUPTIVE_MEASURES,
  findExactDuplicate,
  parseExclusions,
  scenarioMeasuresList,
  scenarioToInsertRow,
  serializeExclusions,
  validateScenarioConfig,
} from "./model";

describe("SCENARIO_MEASURES", () => {
  // Mirror of MeasureType in Hestia-Homes/Model domain/modelling/measure_type.py.
  // The engine CRASHES on exclusion tokens outside this set at modelling time
  // (scenario_table._parse_exclusions is strict), so the two lists must change
  // together — the backend's ADR-0056 shared-contract rule, applied here.
  const BACKEND_MEASURE_TYPES = [
    "cavity_wall_insulation",
    "external_wall_insulation",
    "internal_wall_insulation",
    "loft_insulation",
    "sloping_ceiling_insulation",
    "flat_roof_insulation",
    "suspended_floor_insulation",
    "solid_floor_insulation",
    "double_glazing",
    "secondary_glazing",
    "low_energy_lighting",
    "mechanical_ventilation",
    "high_heat_retention_storage_heaters",
    "air_source_heat_pump",
    "gas_boiler_upgrade",
    "system_tune_up",
    "system_tune_up_zoned",
    "solar_pv",
    "secondary_heating_removal",
  ];

  it("offers exactly the backend MeasureType vocabulary", () => {
    expect([...scenarioMeasuresList].sort()).toEqual([...BACKEND_MEASURE_TYPES].sort());
  });

  it("rejects engine-unparseable tokens at authoring time (the 1275 crash)", () => {
    const result = validateScenarioConfig({
      name: "Low disruption",
      housingType: "Social",
      goal: "Increasing EPC",
      goalValue: "C",
      budgetPerProperty: null,
      exclusions: ["room_roof_insulation"],
    });
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.errors.exclusions).toMatch(/room_roof_insulation/);
  });
});

describe("validateScenarioConfig", () => {
  it("requires a target band when the goal is Increasing EPC", () => {
    const result = validateScenarioConfig({
      name: "Reach EPC C",
      housingType: "Social",
      goal: "Increasing EPC",
      goalValue: null,
      budgetPerProperty: null,
      exclusions: [],
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.errors.goalValue).toMatch(/target/i);
    }
  });

  it("accepts an Increasing EPC config with a valid band", () => {
    const result = validateScenarioConfig({
      name: "Reach EPC C",
      housingType: "Social",
      goal: "Increasing EPC",
      goalValue: "C",
      budgetPerProperty: null,
      exclusions: [],
    });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.goalValue).toBe("C");
    }
  });

  it("accepts a maximise-goal without budget and normalises goalValue to null", () => {
    // "Reducing CO2 emissions" means "as much as possible"; no goal value —
    // and no budget means the engine recommends the maximum interventions.
    const result = validateScenarioConfig({
      name: "Max CO₂ reduction",
      housingType: "Social",
      goal: "Reducing CO2 emissions",
      goalValue: "20%", // stray value must not survive
      budgetPerProperty: null,
      exclusions: [],
    });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.goalValue).toBeNull();
    }
  });

  it("rejects a goal outside the portfolio goal enum", () => {
    const result = validateScenarioConfig({
      name: "X",
      housingType: "Social",
      goal: "Net Zero",
      goalValue: null,
      budgetPerProperty: null,
      exclusions: [],
    });
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.errors.goal).toBeTruthy();
  });

  it("requires a name and trims surrounding whitespace", () => {
    const base = {
      housingType: "Social",
      goal: "Increasing EPC",
      goalValue: "C" as const,
      budgetPerProperty: null,
      exclusions: [],
    };
    const blank = validateScenarioConfig({ ...base, name: "   " });
    expect(blank.ok).toBe(false);
    if (!blank.ok) expect(blank.errors.name).toBeTruthy();

    const padded = validateScenarioConfig({ ...base, name: "  Reach EPC C  " });
    expect(padded.ok).toBe(true);
    if (padded.ok) expect(padded.value.name).toBe("Reach EPC C");
  });

  it("accepts a positive per-property budget and rejects zero or negative", () => {
    const base = {
      name: "Budgeted",
      housingType: "Social",
      goal: "Energy Savings",
      goalValue: null,
      exclusions: [],
    };
    expect(validateScenarioConfig({ ...base, budgetPerProperty: 5000 }).ok).toBe(true);
    const zero = validateScenarioConfig({ ...base, budgetPerProperty: 0 });
    expect(zero.ok).toBe(false);
    if (!zero.ok) expect(zero.errors.budgetPerProperty).toBeTruthy();
    expect(validateScenarioConfig({ ...base, budgetPerProperty: -100 }).ok).toBe(false);
  });

  it("rejects a housing type outside Social/Private", () => {
    const result = validateScenarioConfig({
      name: "X",
      housingType: "Council",
      goal: "Increasing EPC",
      goalValue: "C",
      budgetPerProperty: null,
      exclusions: [],
    });
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.errors.housingType).toBeTruthy();
  });

  const validBase = {
    name: "X",
    housingType: "Social",
    goal: "Increasing EPC",
    goalValue: "C",
    budgetPerProperty: null,
  };

  it("normalises exclusions: dedupes, sorts, and an empty set is valid", () => {
    const result = validateScenarioConfig({
      ...validBase,
      exclusions: ["solid_floor_insulation", "loft_insulation", "solid_floor_insulation"],
    });
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.exclusions).toEqual(["loft_insulation", "solid_floor_insulation"]);
    }
    // empty exclusion set = every measure on the table — explicitly valid
    expect(validateScenarioConfig({ ...validBase, exclusions: [] }).ok).toBe(true);
  });

  it("rejects unknown measure keys", () => {
    const result = validateScenarioConfig({
      ...validBase,
      exclusions: ["asbestos_removal"],
    });
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.errors.exclusions).toMatch(/asbestos_removal/);
  });

  it("rejects excluding every measure — the engine needs at least one option", () => {
    const result = validateScenarioConfig({ ...validBase, exclusions: [...scenarioMeasuresList] });
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.errors.exclusions).toBeTruthy();
  });

  it("carries fabric first through when the scenario is fabric-first", () => {
    const result = validateScenarioConfig({ ...validBase, exclusions: [], fabricFirst: true });
    expect(result.ok).toBe(true);
    if (result.ok) expect(result.value.fabricFirst).toBe(true);
  });

  it("defaults fabric first to false when the flag is absent", () => {
    const result = validateScenarioConfig({ ...validBase, exclusions: [] });
    expect(result.ok).toBe(true);
    if (result.ok) expect(result.value.fabricFirst).toBe(false);
  });
});

describe("scenarioToInsertRow", () => {
  it("maps a validated config to an ADR-0003 app-authored row", () => {
    const result = validateScenarioConfig({
      name: "Reach EPC C",
      housingType: "Social",
      goal: "Increasing EPC",
      goalValue: "C",
      budgetPerProperty: 5000,
      exclusions: ["solid_floor_insulation"],
    });
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    const row = scenarioToInsertRow(result.value, 796n);
    expect(row).toMatchObject({
      portfolioId: 796n,
      name: "Reach EPC C",
      housingType: "Social",
      goal: "Increasing EPC",
      goalValue: "C",
      budget: 5000,
      exclusions: "{solid_floor_insulation}",
      multiPlan: true,     // universal in modern data
      isDefault: false,    // backend owns default-scenario semantics
    });
  });

  it("stores maximise-goals with null goal value, budget and exclusions", () => {
    const result = validateScenarioConfig({
      name: "Max CO₂",
      housingType: "Private",
      goal: "Reducing CO2 emissions",
      goalValue: null,
      budgetPerProperty: null,
      exclusions: [],
    });
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    const row = scenarioToInsertRow(result.value, 42n);
    expect(row.goalValue).toBeNull();
    expect(row.budget).toBeNull();
    expect(row.exclusions).toBeNull();
  });

  it("persists the fabric-first flag, defaulting to false", () => {
    const off = validateScenarioConfig({
      name: "Reach EPC C", housingType: "Social", goal: "Increasing EPC",
      goalValue: "C", budgetPerProperty: null, exclusions: [],
    });
    const on = validateScenarioConfig({
      name: "Reach EPC C — fabric first", housingType: "Social", goal: "Increasing EPC",
      goalValue: "C", budgetPerProperty: null, exclusions: [], fabricFirst: true,
    });
    expect(off.ok && on.ok).toBe(true);
    if (!off.ok || !on.ok) return;
    expect(scenarioToInsertRow(off.value, 1n).fabricFirst).toBe(false);
    expect(scenarioToInsertRow(on.value, 1n).fabricFirst).toBe(true);
  });
});

describe("findExactDuplicate", () => {
  const existing = [
    { id: 1n, name: "Reach EPC C", housingType: "Social", goal: "Increasing EPC",
      goalValue: "C", budget: null, exclusions: "{external_wall_insulation,solid_floor_insulation}",
      fabricFirst: false },
    { id: 2n, name: "Max CO₂", housingType: "Social", goal: "Reducing CO2 emissions",
      goalValue: null, budget: 6000, exclusions: null, fabricFirst: false },
  ];
  const config = {
    name: "A totally different name",
    housingType: "Social",
    goal: "Increasing EPC",
    goalValue: "C" as string | null,
    budgetPerProperty: null as number | null,
    exclusions: ["solid_floor_insulation", "external_wall_insulation"],
  };

  it("matches on configuration regardless of name or exclusion order", () => {
    expect(findExactDuplicate(config, existing)?.id).toBe(1n);
  });

  it("does not match when any configuration facet differs", () => {
    expect(findExactDuplicate({ ...config, goalValue: "B" }, existing)).toBeNull();
    expect(findExactDuplicate({ ...config, budgetPerProperty: 5000 }, existing)).toBeNull();
    expect(findExactDuplicate({ ...config, housingType: "Private" }, existing)).toBeNull();
    expect(
      findExactDuplicate({ ...config, exclusions: ["solid_floor_insulation"] }, existing),
    ).toBeNull();
  });

  it("treats fabric first as part of the configuration identity", () => {
    // id 1 is not fabric-first; the same config asked fabric-first is a distinct question.
    expect(findExactDuplicate({ ...config, fabricFirst: true }, existing)).toBeNull();
    // ...and matches once both sides agree it is fabric-first.
    const fabricFirstExisting = existing.map((s) =>
      s.id === 1n ? { ...s, fabricFirst: true } : s,
    );
    expect(
      findExactDuplicate({ ...config, fabricFirst: true }, fabricFirstExisting)?.id,
    ).toBe(1n);
  });
});

describe("DISRUPTIVE_MEASURES", () => {
  it("only marks real measure keys (a typo would silently drop the badge)", () => {
    for (const key of Object.keys(DISRUPTIVE_MEASURES)) {
      expect(scenarioMeasuresList as readonly string[]).toContain(key);
    }
  });
});

describe("display helpers", () => {
  it("derives status from plan existence — no stored status column", () => {
    expect(deriveScenarioStatus(0)).toBe("awaiting_modelling");
    expect(deriveScenarioStatus(292)).toBe("modelled");
  });

  it("frames constraints from whichever side is shorter", () => {
    expect(constraintSummary([])).toEqual({ kind: "all" });
    expect(constraintSummary(["solid_floor_insulation"])).toEqual({
      kind: "excludes",
      measures: ["Solid Floor Insulation"],
    });
    // 18 exclusions → say what's allowed instead (in canonical measure order)
    const onlyTwo = scenarioMeasuresList.filter(
      (k) => k !== "solar_pv" && k !== "air_source_heat_pump",
    );
    expect(constraintSummary(onlyTwo)).toEqual({
      kind: "only",
      measures: ["Air Source Heat Pump", "Solar PV"],
    });
  });
});

describe("exclusions text format", () => {
  it("serialises to the backend's brace format and round-trips", () => {
    const keys = ["loft_insulation", "solid_floor_insulation"];
    const text = serializeExclusions(keys);
    expect(text).toBe("{loft_insulation,solid_floor_insulation}");
    expect(parseExclusions(text)).toEqual(keys);
  });

  it("serialises the empty set as null and parses null/empty as no exclusions", () => {
    expect(serializeExclusions([])).toBeNull();
    expect(parseExclusions(null)).toEqual([]);
    expect(parseExclusions("{}")).toEqual([]);
  });

  it("parses the legacy space-padded variant the backend also writes", () => {
    // e.g. scenario 1270: "{external_wall_insulation, internal_wall_insulation, ...}"
    expect(
      parseExclusions("{external_wall_insulation, internal_wall_insulation}"),
    ).toEqual(["external_wall_insulation", "internal_wall_insulation"]);
  });
});
