import { describe, expect, it } from "vitest";
import {
  buildWritePlan,
  cacheAgeDays,
  classifyOsCode,
  describeCandidate,
  isCacheFresh,
  normalisePostcode,
  postcodeCacheKeys,
  toAddressCandidates,
} from "./model";

describe("classifyOsCode", () => {
  it("maps the five confident residential codes per ADR-0007", () => {
    expect(classifyOsCode("RD02")).toEqual({ selectable: true, propertyType: "House", builtForm: "Detached" });
    expect(classifyOsCode("RD03")).toEqual({ selectable: true, propertyType: "House", builtForm: "Semi-Detached" });
    // Terraced: mid/end unknowable — type only
    expect(classifyOsCode("RD04")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
    expect(classifyOsCode("RD06")).toEqual({ selectable: true, propertyType: "Flat", builtForm: null });
    expect(classifyOsCode("RD07")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
  });

  it("maps the RH (HMO) family to House, like RD07", () => {
    // e.g. UPRN 100031252876, 7 Leyton Avenue — RH03 must not read as non-residential.
    expect(classifyOsCode("RH01")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
    expect(classifyOsCode("RH02")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
    expect(classifyOsCode("RH03")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
  });

  it("keeps other residential codes selectable but writes no facts — never Unknown", () => {
    for (const code of ["RD", "RD01", "RD08"]) {
      expect(classifyOsCode(code)).toEqual({ selectable: true, propertyType: null, builtForm: null });
    }
  });

  it("marks non-residential and institutional codes unselectable", () => {
    expect(classifyOsCode("CR08").selectable).toBe(false); // commercial retail
    expect(classifyOsCode("RD10").selectable).toBe(false); // residential institution
    expect(classifyOsCode(null).selectable).toBe(false);
  });
});

describe("isCacheFresh", () => {
  it("serves cache younger than 30 days, refreshes older", () => {
    const now = new Date("2026-07-06T12:00:00Z");
    expect(isCacheFresh(new Date("2026-06-10T12:00:00Z"), now)).toBe(true);
    expect(isCacheFresh(new Date("2026-06-06T11:00:00Z"), now)).toBe(false);
    expect(isCacheFresh(null, now)).toBe(false);
  });
});

describe("toAddressCandidates", () => {
  const items = [
    { DPA: { UPRN: "100", ADDRESS: "16 ALDER GROVE, MANCHESTER, M20 4TF", POSTCODE: "M20 4TF",
             CLASSIFICATION_CODE: "RD02", LAT: 53.43, LNG: -2.23 } },
    // LPI-only record — still usable
    { LPI: { UPRN: "101", ADDRESS: "18 ALDER GROVE, MANCHESTER, M20 4TF", POSTCODE_LOCATOR: "M20 4TF",
             CLASSIFICATION_CODE: "CR08", LAT: 53.44, LNG: -2.24 } },
    // duplicate UPRN (DPA + LPI for same address) — deduped, DPA wins
    { LPI: { UPRN: "100", ADDRESS: "16 ALDER GROVE ALT, M20 4TF", CLASSIFICATION_CODE: "RD02" } },
  ];
  it("prefers DPA, dedupes by UPRN, strips the postcode from the address", () => {
    const out = toAddressCandidates(items as never, "M20 4TF");
    expect(out).toHaveLength(2);
    expect(out[0]).toMatchObject({
      uprn: "100", address: "16 Alder Grove, Manchester", postcode: "M20 4TF",
      classificationCode: "RD02", lat: 53.43, lng: -2.23, selectable: true,
      propertyType: "House", builtForm: "Detached",
    });
    expect(out[1]).toMatchObject({ uprn: "101", selectable: false });
  });

  it("classifies an RH03 HMO as a selectable House, not Non-residential", () => {
    // Regression: 7 Leyton Avenue (UPRN 100031252876) came back RH03 ("HMO Not
    // Further Divided") on a street of RD02/RD03 houses and rendered as
    // "Non-residential", so it couldn't be added.
    const hmo = [
      { DPA: { UPRN: "100031252876", ADDRESS: "7, LEYTON AVENUE, SUTTON-IN-ASHFIELD, NG17 3BD",
               POSTCODE: "NG17 3BD", CLASSIFICATION_CODE: "RH03", LAT: 53.1352313, LNG: -1.2522665 } },
    ];
    const out = toAddressCandidates(hmo as never, "NG17 3BD");
    expect(out[0]).toMatchObject({
      uprn: "100031252876", address: "7, Leyton Avenue, Sutton-In-Ashfield",
      classificationCode: "RH03", selectable: true, propertyType: "House", builtForm: null,
    });
  });

  it("orders addresses numerically, so Flat 2 comes before Flat 10", () => {
    const flats = [
      { DPA: { UPRN: "1", ADDRESS: "FLAT 10, BIRCH COURT, M20 4TF", CLASSIFICATION_CODE: "RD06" } },
      { DPA: { UPRN: "2", ADDRESS: "FLAT 2, BIRCH COURT, M20 4TF", CLASSIFICATION_CODE: "RD06" } },
      { DPA: { UPRN: "3", ADDRESS: "FLAT 1, BIRCH COURT, M20 4TF", CLASSIFICATION_CODE: "RD06" } },
      { DPA: { UPRN: "4", ADDRESS: "3 BIRCH LANE, M20 4TF", CLASSIFICATION_CODE: "RD02" } },
      { DPA: { UPRN: "5", ADDRESS: "12 BIRCH LANE, M20 4TF", CLASSIFICATION_CODE: "RD02" } },
    ];
    const out = toAddressCandidates(flats as never, "M20 4TF");
    expect(out.map((c) => c.address)).toEqual([
      "3 Birch Lane",
      "12 Birch Lane",
      "Flat 1, Birch Court",
      "Flat 2, Birch Court",
      "Flat 10, Birch Court",
    ]);
  });
});

describe("buildWritePlan", () => {
  const geo = { localAuthority: "Manchester", constituency: "Manchester Withington" };
  it("builds the property row and both override layers for a mapped candidate", () => {
    const plan = buildWritePlan(
      { uprn: "100", address: "16 Alder Grove, Manchester", postcode: "M20 4TF",
        classificationCode: "RD02", lat: 53.43, lng: -2.23, selectable: true,
        propertyType: "House", builtForm: "Detached" },
      geo,
    );
    expect(plan.property).toMatchObject({
      address: "16 Alder Grove, Manchester", postcode: "M20 4TF", uprn: 100n,
      latitude: 53.43, longitude: -2.23,
      localAuthority: "Manchester", constituency: "Manchester Withington",
    });
    // both facts, snapshot keyed to the OS code, main building part
    expect(plan.overrides).toEqual([
      { component: "property_type", value: "House", originalDescription: "RD02", buildingPart: 0 },
      { component: "built_form_type", value: "Detached", originalDescription: "RD02", buildingPart: 0 },
    ]);
  });
  it("writes a House property_type override for an RH03 HMO, keyed to the OS code", () => {
    const plan = buildWritePlan(
      { uprn: "100031252876", address: "7, Leyton Avenue, Sutton-In-Ashfield", postcode: "NG17 3BD",
        classificationCode: "RH03", lat: 53.1352313, lng: -1.2522665, selectable: true,
        propertyType: "House", builtForm: null },
      geo,
    );
    // HMO built form is unknowable, so only the property_type fact is written.
    expect(plan.overrides).toEqual([
      { component: "property_type", value: "House", originalDescription: "RH03", buildingPart: 0 },
    ]);
  });
  it("writes no overrides when the code maps nothing — never Unknown", () => {
    const plan = buildWritePlan(
      { uprn: "200", address: "Birch Court", postcode: "M20 4UD", classificationCode: "RD",
        lat: null, lng: null, selectable: true, propertyType: null, builtForm: null },
      geo,
    );
    expect(plan.overrides).toEqual([]);
  });
});

describe("describeCandidate", () => {
  const base = { selectable: true, propertyType: null, builtForm: null, classificationCode: null };
  it("labels fully-mapped candidates with both facts", () => {
    expect(describeCandidate({ ...base, classificationCode: "RD02", propertyType: "House", builtForm: "Detached" }))
      .toEqual({ label: "House · Detached", note: null });
  });
  it("explains why terraced houses carry no built form", () => {
    expect(describeCandidate({ ...base, classificationCode: "RD04", propertyType: "House" }))
      .toEqual({ label: "House", note: "Terraced — mid or end unknown" });
    // Other single-fact codes carry no note
    expect(describeCandidate({ ...base, classificationCode: "RD07", propertyType: "House" }))
      .toEqual({ label: "House", note: null });
    expect(describeCandidate({ ...base, classificationCode: "RD06", propertyType: "Flat" }))
      .toEqual({ label: "Flat", note: null });
  });
  it("labels selectable-but-unmapped codes Residential, with the modelling note", () => {
    expect(describeCandidate({ ...base, classificationCode: "RD" }))
      .toEqual({ label: "Residential", note: "No classification — type comes with modelling" });
  });
  it("labels unselectable candidates Non-residential", () => {
    expect(describeCandidate({ ...base, selectable: false, classificationCode: "CR08" }))
      .toEqual({ label: "Non-residential", note: null });
  });
});

describe("postcodeCacheKeys", () => {
  it("reads the canonical spaced form first, then the legacy space-less form", () => {
    expect(postcodeCacheKeys("M20 4TF")).toEqual(["M20 4TF", "M204TF"]);
  });
});

describe("cacheAgeDays", () => {
  const now = new Date("2026-07-06T12:00:00Z");
  it("floors to whole days", () => {
    expect(cacheAgeDays(new Date("2026-07-03T11:00:00Z"), now)).toBe(3);
    expect(cacheAgeDays(new Date("2026-07-06T09:00:00Z"), now)).toBe(0);
  });
});

describe("normalisePostcode", () => {
  it("uppercases and collapses to the canonical single-space form", () => {
    expect(normalisePostcode("m20  4tf")).toBe("M20 4TF");
    expect(normalisePostcode(" sw1a1aa ")).toBe("SW1A 1AA");
    expect(normalisePostcode("M20 4TF")).toBe("M20 4TF");
  });

  it("returns null for strings that are not plausible postcodes", () => {
    expect(normalisePostcode("")).toBeNull();
    expect(normalisePostcode("not a postcode")).toBeNull();
    expect(normalisePostcode("12345")).toBeNull();
  });
});
