import { describe, expect, it } from "vitest";
import {
  resolveBuiltForm,
  resolveConstructionAge,
  resolveMainFuel,
  resolveOverrideDescriptor,
  resolvePropertyType,
} from "./descriptorResolution";
import {
  MAINFUEL_OPTIONS,
  RDSAP_MAIN_FUEL_CODE_LABELS,
  EPR_MAIN_FUEL_STRING_LABELS,
  WALL_TYPE_OPTIONS,
  ROOF_TYPE_OPTIONS,
  HEATING_SYSTEM_OPTIONS,
  DESCRIPTOR_FILTER_CONFIG,
  classifyDescriptor,
} from "@/app/utils/propertyFilters";
import {
  MainFuelValues,
  WallTypeValues,
  RoofTypeValues,
  MainHeatingSystemValues,
} from "@/app/db/schema/landlord_overrides";

describe("resolvePropertyType", () => {
  it("prefers a landlord override over the EPC-derived value", () => {
    expect(
      resolvePropertyType({
        override: "Maisonette",
        isNewApproach: true,
        lodgedPropertyType: "2", // EPC code for Flat
        lodgedDwellingType: null,
        predictedPropertyType: null,
        predictedDwellingType: null,
        legacyPropertyType: null,
      }),
    ).toBe("Maisonette");
  });

  it("maps an RdSAP property-type code to its label when there is no override", () => {
    expect(
      resolvePropertyType({
        override: null,
        isNewApproach: true,
        lodgedPropertyType: "0", // RdSAP code for House
        lodgedDwellingType: null,
        predictedPropertyType: null,
        predictedDwellingType: null,
        legacyPropertyType: null,
      }),
    ).toBe("House");
  });

  it("falls back to the predicted EPC when there is no lodged value", () => {
    expect(
      resolvePropertyType({
        override: null,
        isNewApproach: true,
        lodgedPropertyType: null,
        lodgedDwellingType: null,
        predictedPropertyType: "2", // RdSAP code for Flat
        predictedDwellingType: null,
        legacyPropertyType: null,
      }),
    ).toBe("Flat");
  });

  it("prefers the lodged EPC over the predicted EPC when both exist", () => {
    expect(
      resolvePropertyType({
        override: null,
        isNewApproach: true,
        lodgedPropertyType: "0", // House
        lodgedDwellingType: null,
        predictedPropertyType: "2", // Flat
        predictedDwellingType: null,
        legacyPropertyType: null,
      }),
    ).toBe("House");
  });

  it("falls back to dwelling_type when the lodged property_type is absent", () => {
    expect(
      resolvePropertyType({
        override: null,
        isNewApproach: true,
        lodgedPropertyType: null,
        lodgedDwellingType: "Flat",
        predictedPropertyType: null,
        predictedDwellingType: null,
        legacyPropertyType: null,
      }),
    ).toBe("Flat");
  });

  it("reads the legacy property-row value for a legacy property", () => {
    expect(
      resolvePropertyType({
        override: null,
        isNewApproach: false,
        lodgedPropertyType: "0", // must be ignored for legacy
        lodgedDwellingType: null,
        predictedPropertyType: null,
        predictedDwellingType: null,
        legacyPropertyType: "Bungalow",
      }),
    ).toBe("Bungalow");
  });

  it("never reads the EPC graph nor the legacy row across the cutoff (new-approach ignores the legacy row)", () => {
    expect(
      resolvePropertyType({
        override: null,
        isNewApproach: true,
        lodgedPropertyType: null,
        lodgedDwellingType: null,
        predictedPropertyType: null,
        predictedDwellingType: null,
        legacyPropertyType: "Bungalow", // legacy row must NOT surface for a new-approach property
      }),
    ).toBe("Unknown");
  });

  // Spec locks for decisions taken in ADR-0012 — an override always wins (even an
  // explicit "Unknown"), and an unrecognised RdSAP code is passed through as-is.
  it("lets an explicit 'Unknown' override win over a known EPC value", () => {
    expect(
      resolvePropertyType({
        override: "Unknown",
        isNewApproach: true,
        lodgedPropertyType: "0", // EPC knows "House" — the override still wins
        lodgedDwellingType: null,
        predictedPropertyType: null,
        predictedDwellingType: null,
        legacyPropertyType: null,
      }),
    ).toBe("Unknown");
  });

  it("passes an unrecognised RdSAP code through unchanged", () => {
    expect(
      resolvePropertyType({
        override: null,
        isNewApproach: true,
        lodgedPropertyType: "9", // not in PROPERTY_TYPE_LABELS
        lodgedDwellingType: null,
        predictedPropertyType: null,
        predictedDwellingType: null,
        legacyPropertyType: null,
      }),
    ).toBe("9");
  });
});

describe("resolveBuiltForm", () => {
  it("prefers a landlord override over the EPC-derived value", () => {
    expect(
      resolveBuiltForm({
        override: "Detached",
        isNewApproach: true,
        lodgedBuiltForm: "4", // EPC code for Mid-Terrace
        predictedBuiltForm: null,
        legacyBuiltForm: null,
      }),
    ).toBe("Detached");
  });

  it("maps an RdSAP built-form code to its label when there is no override", () => {
    expect(
      resolveBuiltForm({
        override: null,
        isNewApproach: true,
        lodgedBuiltForm: "2", // RdSAP code for Semi-Detached
        predictedBuiltForm: null,
        legacyBuiltForm: null,
      }),
    ).toBe("Semi-Detached");
  });

  it("falls back to the predicted EPC when there is no lodged value", () => {
    expect(
      resolveBuiltForm({
        override: null,
        isNewApproach: true,
        lodgedBuiltForm: null,
        predictedBuiltForm: "1", // Detached
        legacyBuiltForm: null,
      }),
    ).toBe("Detached");
  });

  it("reads the legacy property-row value for a legacy property", () => {
    expect(
      resolveBuiltForm({
        override: null,
        isNewApproach: false,
        lodgedBuiltForm: "1", // must be ignored for legacy
        predictedBuiltForm: null,
        legacyBuiltForm: "End-Terrace",
      }),
    ).toBe("End-Terrace");
  });

  it("resolves to 'Unknown' when nothing is available (never null)", () => {
    expect(
      resolveBuiltForm({
        override: null,
        isNewApproach: true,
        lodgedBuiltForm: null,
        predictedBuiltForm: null,
        legacyBuiltForm: "Detached", // legacy row must NOT surface for new-approach
      }),
    ).toBe("Unknown");
  });
});

describe("resolveConstructionAge", () => {
  it("maps a landlord override band letter to its band label", () => {
    expect(
      resolveConstructionAge({
        override: "B",
        isNewApproach: true,
        lodgedAgeBand: "F", // EPC says a different band — override still wins
        predictedAgeBand: null,
        legacyYearBuilt: null,
      }),
    ).toBe("1900–1929");
  });

  it("maps the lodged EPC band letter to its label when there is no override", () => {
    expect(
      resolveConstructionAge({
        override: null,
        isNewApproach: true,
        lodgedAgeBand: "C",
        predictedAgeBand: null,
        legacyYearBuilt: null,
      }),
    ).toBe("1930–1949");
  });

  it("falls back to the predicted EPC band when there is no lodged band", () => {
    expect(
      resolveConstructionAge({
        override: null,
        isNewApproach: true,
        lodgedAgeBand: null,
        predictedAgeBand: "M",
        legacyYearBuilt: null,
      }),
    ).toBe("2023 onwards");
  });

  it("buckets a legacy year_built into its band (mid-range)", () => {
    expect(legacyAge("1985")).toBe("1983–1990");
  });

  it("buckets a legacy year before 1900 into band A", () => {
    expect(legacyAge("1899")).toBe("before 1900");
  });

  it("buckets the first year of a band correctly (boundary)", () => {
    expect(legacyAge("1900")).toBe("1900–1929");
  });

  it("buckets a recent legacy year into the open-ended final band", () => {
    expect(legacyAge("2025")).toBe("2023 onwards");
  });

  it("resolves a non-numeric legacy year_built to 'Unknown'", () => {
    expect(legacyAge("N/A")).toBe("Unknown");
  });

  it("resolves to 'Unknown' when nothing is available; ignores the legacy row for new-approach", () => {
    expect(
      resolveConstructionAge({
        override: null,
        isNewApproach: true,
        lodgedAgeBand: null,
        predictedAgeBand: null,
        legacyYearBuilt: "1985", // must NOT surface for a new-approach property
      }),
    ).toBe("Unknown");
  });

  it("lets an explicit 'Unknown' override win", () => {
    expect(
      resolveConstructionAge({
        override: "Unknown",
        isNewApproach: true,
        lodgedAgeBand: "C",
        predictedAgeBand: null,
        legacyYearBuilt: null,
      }),
    ).toBe("Unknown");
  });
});

describe("resolveMainFuel", () => {
  const base = {
    override: null,
    isNewApproach: true,
    lodgedMainFuel: null,
    predictedMainFuel: null,
    legacyMainFuel: null,
  };

  it("prefers a landlord override over the EPC-derived value", () => {
    expect(
      resolveMainFuel({ ...base, override: "mains gas", lodgedMainFuel: "26" }),
    ).toBe("mains gas");
  });

  it("reads the new-approach EPC main-fuel code (lodged) as a raw value", () => {
    expect(resolveMainFuel({ ...base, lodgedMainFuel: "26" })).toBe("26");
  });

  it("falls back to the predicted EPC main fuel when there is no lodged one", () => {
    expect(resolveMainFuel({ ...base, predictedMainFuel: "29" })).toBe("29");
  });

  it("prefers the lodged EPC main fuel over the predicted one", () => {
    expect(
      resolveMainFuel({ ...base, lodgedMainFuel: "26", predictedMainFuel: "29" }),
    ).toBe("26");
  });

  it("passes an EPR string main fuel through unchanged", () => {
    expect(resolveMainFuel({ ...base, lodgedMainFuel: "Mains gas" })).toBe("Mains gas");
  });

  it("resolves to 'Unknown' for a new-approach property with no EPC main fuel; ignores the legacy row", () => {
    expect(
      resolveMainFuel({ ...base, legacyMainFuel: "Gas mains gas" }),
    ).toBe("Unknown");
  });

  it("reads the legacy EPC main fuel for a legacy property", () => {
    expect(
      resolveMainFuel({
        ...base,
        isNewApproach: false,
        lodgedMainFuel: "26", // must be ignored for legacy
        legacyMainFuel: "Gas mains gas",
      }),
    ).toBe("Gas mains gas");
  });

  it("resolves to 'Unknown' when nothing is available (never null)", () => {
    expect(resolveMainFuel({ ...base, isNewApproach: false })).toBe("Unknown");
  });
});

describe("MAINFUEL_OPTIONS coverage", () => {
  const covered = new Set(MAINFUEL_OPTIONS.flatMap((o) => o.dbValues));
  const labels = new Set(MAINFUEL_OPTIONS.map((o) => o.label));

  // Every landlord main-fuel override value must fold into a filter option, or an
  // overridden fuel would display but be unfilterable (ADR-0012).
  it("covers every landlord main-fuel override value", () => {
    for (const value of MainFuelValues) {
      expect(covered).toContain(value);
    }
  });

  // Every RdSAP main_fuel code the new EPC graph can store must fold into a
  // filter option under a real label (a typo'd label would be silently dropped
  // by the MAINFUEL_OPTIONS merge — assert both the code AND its label).
  it("folds every RdSAP main_fuel code under a real label", () => {
    for (const [code, label] of Object.entries(RDSAP_MAIN_FUEL_CODE_LABELS)) {
      expect(labels).toContain(label);
      expect(covered).toContain(code);
    }
  });

  // Every EPR free-text main_fuel string must fold in too.
  it("folds every EPR main-fuel string under a real label", () => {
    for (const [str, label] of Object.entries(EPR_MAIN_FUEL_STRING_LABELS)) {
      expect(labels).toContain(label);
      expect(covered).toContain(str);
    }
  });
});

describe("resolveOverrideDescriptor (wall / roof / heating free-text)", () => {
  const base = {
    override: null,
    isNewApproach: true,
    lodgedDescription: null,
    predictedDescription: null,
    legacyDescription: null,
  };

  it("prefers a landlord override over the EPC description", () => {
    expect(
      resolveOverrideDescriptor({
        ...base,
        override: "Cavity wall, filled cavity",
        lodgedDescription: "Solid brick, as built, no insulation (assumed)",
      }),
    ).toBe("Cavity wall, filled cavity");
  });

  it("uses the lodged EPC description when there is no override", () => {
    expect(
      resolveOverrideDescriptor({
        ...base,
        lodgedDescription: "Pitched, 250 mm loft insulation",
      }),
    ).toBe("Pitched, 250 mm loft insulation");
  });

  it("falls back to the predicted EPC description when there is no lodged one", () => {
    expect(
      resolveOverrideDescriptor({ ...base, predictedDescription: "Flat, insulated (assumed)" }),
    ).toBe("Flat, insulated (assumed)");
  });

  it("reads the legacy description for a legacy property", () => {
    expect(
      resolveOverrideDescriptor({
        ...base,
        isNewApproach: false,
        lodgedDescription: "ignored for legacy",
        legacyDescription: "Boiler and radiators, mains gas",
      }),
    ).toBe("Boiler and radiators, mains gas");
  });

  it("resolves to 'Unknown' when nothing is available; ignores legacy for new-approach", () => {
    expect(
      resolveOverrideDescriptor({ ...base, legacyDescription: "must not surface" }),
    ).toBe("Unknown");
  });
});

describe("classifyDescriptor — wall / roof / heating buckets", () => {
  const wall = (v: string) =>
    classifyDescriptor(v, WALL_TYPE_OPTIONS, DESCRIPTOR_FILTER_CONFIG.wallType.match);
  const roof = (v: string) =>
    classifyDescriptor(v, ROOF_TYPE_OPTIONS, DESCRIPTOR_FILTER_CONFIG.roofType.match);
  const heat = (v: string) =>
    classifyDescriptor(v, HEATING_SYSTEM_OPTIONS, DESCRIPTOR_FILTER_CONFIG.heatingSystem.match);

  // Every landlord override value must fall into a real (non-Unknown) bucket, or
  // an overridden component would display but be unfilterable (ADR-0012). The
  // literal "Unknown" is the sole exception — it maps to the Unknown bucket.
  const assertVocabCovered = (
    values: readonly string[],
    classify: (v: string) => string,
    labels: Set<string>,
  ) => {
    for (const value of values) {
      const bucket = classify(value);
      expect(labels).toContain(bucket);
      if (value !== "Unknown") expect(bucket).not.toBe("Unknown");
    }
  };

  it("buckets every WallType override value", () => {
    assertVocabCovered(WallTypeValues, wall, new Set(WALL_TYPE_OPTIONS.map((b) => b.label)));
  });
  it("buckets every RoofType override value", () => {
    assertVocabCovered(RoofTypeValues, roof, new Set(ROOF_TYPE_OPTIONS.map((b) => b.label)));
  });
  it("buckets every MainHeatingSystem override value", () => {
    assertVocabCovered(
      MainHeatingSystemValues,
      heat,
      new Set(HEATING_SYSTEM_OPTIONS.map((b) => b.label)),
    );
  });

  // Representative EPC / legacy descriptions (from production) bucket as expected.
  it("buckets representative EPC/legacy WALL descriptions", () => {
    expect(wall("Cavity wall, filled cavity")).toBe("Cavity");
    expect(wall("Solid brick, as built, no insulation (assumed)")).toBe("Solid Brick");
    expect(wall("Timber frame, as built, insulated (assumed)")).toBe("Timber Frame");
    expect(wall("Sandstone or limestone, as built, no insulation (assumed)")).toBe("Stone");
    expect(wall("Granite or whinstone, as built, no insulation (assumed)")).toBe("Stone");
    expect(wall("System built, with external insulation")).toBe("System Built");
    // A bare U-value is not a construction type → Unknown.
    expect(wall("Average thermal transmittance 0.21 W/m²K")).toBe("Unknown");
  });

  it("buckets representative EPC/legacy ROOF descriptions", () => {
    expect(roof("Pitched, 200 mm loft insulation")).toBe("Pitched");
    expect(roof("Flat, no insulation (assumed)")).toBe("Flat");
    expect(roof("Roof room(s), insulated (assumed)")).toBe("Room-in-Roof");
    expect(roof("(another dwelling above)")).toBe("Premises Above");
    expect(roof("(other premises above)")).toBe("Premises Above");
    expect(roof("Another Premises Above")).toBe("Premises Above");
    expect(roof("Average thermal transmittance 0.11 W/m²K")).toBe("Unknown");
  });

  it("buckets representative EPC/legacy HEATING descriptions", () => {
    expect(heat("Boiler and radiators, mains gas")).toBe("Gas Boiler");
    expect(heat("Room heaters, mains gas")).toBe("Gas Room Heater");
    expect(heat("Community scheme")).toBe("Community");
    expect(heat("Community scheme, biomass")).toBe("Community");
    expect(heat("Electric storage heaters")).toBe("Electric Storage");
    expect(heat("Room heaters, electric")).toBe("Direct Electric");
    expect(heat("Air source heat pump, radiators, electric")).toBe("Heat Pump");
    // "oil" is a substring of "boiler" — a gas boiler must NOT bucket as Oil.
    expect(heat("Boiler and radiators, oil")).toBe("Oil");
    expect(heat("Boiler and radiators, dual fuel (mineral and wood)")).toBe("Solid Fuel");
    expect(heat("Boiler and radiators, lpg")).toBe("LPG");
  });
});

/** Legacy-property helper: only year_built is set, cutoff is false. */
function legacyAge(year: string): string {
  return resolveConstructionAge({
    override: null,
    isNewApproach: false,
    lodgedAgeBand: null,
    predictedAgeBand: null,
    legacyYearBuilt: year,
  });
}
