import { describe, expect, it } from "vitest";
import {
  buildHeadline,
  carsOffTheRoad,
  classifyBandMovement,
  computeLedger,
  isCompliantBeyondWindow,
  pickBestIndex,
  selectGoalCallout,
  shapeKpiDelta,
  toBandCounts,
  TONNES_CO2_PER_CAR_PER_YEAR,
} from "./model";

describe("computeLedger", () => {
  it("computes all-in gross and net from scenario aggregates", () => {
    // CONTEXT.md: gross = construction + project delivery + contingency;
    // net = gross − funding. Delivery is the disclosed 30% assumption.
    expect(
      computeLedger({
        constructionCost: 1_000_000,
        contingency: 100_000,
        funding: 200_000,
        homesUpgraded: 100,
      }),
    ).toEqual({
      constructionCost: 1_000_000,
      projectDelivery: 300_000,
      contingency: 100_000,
      funding: 200_000,
      grossCost: 1_400_000,
      netCost: 1_200_000,
      grossPerHome: 14_000,
    });
  });

  it("reports zero gross-per-home when nothing is upgraded, not a division blow-up", () => {
    const ledger = computeLedger({
      constructionCost: 0,
      contingency: 0,
      funding: 0,
      homesUpgraded: 0,
    });
    expect(ledger.grossPerHome).toBe(0);
    expect(ledger.grossCost).toBe(0);
    expect(ledger.netCost).toBe(0);
  });

  it("lets funding exceed gross — a surplus reads as negative net, never clamped", () => {
    const ledger = computeLedger({
      constructionCost: 100_000,
      contingency: 0,
      funding: 200_000,
      homesUpgraded: 10,
    });
    expect(ledger.grossCost).toBe(130_000);
    expect(ledger.netCost).toBe(-70_000);
  });
});

describe("isCompliantBeyondWindow", () => {
  const window = { band: "C", date: new Date("2030-01-01") } as const;

  it("skips a lodged home at the window band whose certificate outlives the window", () => {
    expect(
      isCompliantBeyondWindow(
        {
          provenance: "lodged",
          lodgedBand: "C",
          expiryDate: new Date("2031-06-15"),
        },
        window,
      ),
    ).toBe(true);
  });

  it("keeps a home below the window band even with a long-lived certificate", () => {
    expect(
      isCompliantBeyondWindow(
        {
          provenance: "lodged",
          lodgedBand: "D",
          expiryDate: new Date("2033-01-01"),
        },
        window,
      ),
    ).toBe(false);
  });

  it("never skips a predicted home — mirrored lodged estimates are not a certificate", () => {
    // CONTEXT.md (EPC provenance): predicted homes carry lodged_* columns
    // with mirrored estimates, so band + expiry alone must not qualify them.
    expect(
      isCompliantBeyondWindow(
        {
          provenance: "predicted",
          lodgedBand: "B",
          expiryDate: new Date("2035-01-01"),
        },
        window,
      ),
    ).toBe(false);
  });

  it("keeps a home whose certificate expires exactly on the window date", () => {
    expect(
      isCompliantBeyondWindow(
        {
          provenance: "lodged",
          lodgedBand: "B",
          expiryDate: new Date("2030-01-01"),
        },
        window,
      ),
    ).toBe(false);
  });

  it("keeps a lodged home with no expiry on record", () => {
    expect(
      isCompliantBeyondWindow(
        { provenance: "lodged", lodgedBand: "B", expiryDate: null },
        window,
      ),
    ).toBe(false);
  });

  it("skips at better-than-window bands, using a custom window", () => {
    expect(
      isCompliantBeyondWindow(
        {
          provenance: "lodged",
          lodgedBand: "A",
          expiryDate: new Date("2032-07-01"),
        },
        { band: "B", date: new Date("2031-12-31") },
      ),
    ).toBe(true);
  });
});

describe("classifyBandMovement", () => {
  // CONTEXT.md: Likely downgrade / Likely upgrade are band-movement signals
  // between Lodged and Effective. SAP drift within a band never qualifies —
  // structurally so: the classifier only sees bands.

  it("flags a likely downgrade when the certificate flatters the home", () => {
    expect(
      classifyBandMovement({
        provenance: "lodged",
        lodgedBand: "C",
        effectiveBand: "D",
      }),
    ).toBe("likely-downgrade");
  });

  it("flags a likely upgrade when the modelled picture beats the certificate", () => {
    expect(
      classifyBandMovement({
        provenance: "lodged",
        lodgedBand: "D",
        effectiveBand: "C",
      }),
    ).toBe("likely-upgrade");
  });

  it("reports no movement when lodged and effective agree on the band", () => {
    expect(
      classifyBandMovement({
        provenance: "lodged",
        lodgedBand: "D",
        effectiveBand: "D",
      }),
    ).toBe("none");
  });

  it("never signals a predicted home — no certificate, nothing to re-survey against", () => {
    expect(
      classifyBandMovement({
        provenance: "predicted",
        lodgedBand: "D",
        effectiveBand: "C",
      }),
    ).toBe("none");
  });

  it("stays silent when either band is missing", () => {
    expect(
      classifyBandMovement({
        provenance: "lodged",
        lodgedBand: null,
        effectiveBand: "C",
      }),
    ).toBe("none");
    expect(
      classifyBandMovement({
        provenance: "lodged",
        lodgedBand: "C",
        effectiveBand: null,
      }),
    ).toBe("none");
  });
});

describe("pickBestIndex", () => {
  // Compare view (Screen D): mark the best value in each row without
  // crowning an overall winner. Nulls (baseline / missing) never win.

  it("picks the lowest for a lower-is-better row", () => {
    expect(pickBestIndex([null, 50, 74, 103], "lower")).toBe(1);
  });

  it("picks the highest for a higher-is-better row", () => {
    expect(pickBestIndex([null, 312, 368, 219], "higher")).toBe(2);
  });

  it("returns null when every comparable value is missing", () => {
    expect(pickBestIndex([null, null], "lower")).toBeNull();
  });

  it("ignores nulls rather than treating them as zero", () => {
    expect(pickBestIndex([null, null, 5], "lower")).toBe(2);
  });
});

describe("toBandCounts", () => {
  it("totals actual + estimated per band into a plain record", () => {
    expect(
      toBandCounts([
        { epc: "C", actual: 68, estimated: 12 },
        { epc: "D", actual: 104, estimated: 24 },
        { epc: "Unknown", actual: 6, estimated: 0 },
      ]),
    ).toEqual({ C: 80, D: 128, Unknown: 6 });
  });
});

describe("selectGoalCallout", () => {
  // Grilling decision: current stock follows portfolio.goal; the scenario
  // view follows the Scenario's goal. EPC goals at portfolio level default
  // to band C (the portfolio carries no band value).
  const bandCounts = { B: 14, C: 92, D: 128, E: 64, F: 18, G: 8, Unknown: 6 };

  it("gives an EPC-goal portfolio its homes-below-band count on current stock", () => {
    expect(
      selectGoalCallout({
        view: "current-stock",
        goal: "Increasing EPC",
        bandCounts,
      }),
    ).toEqual({ kind: "below-band", band: "C", count: 218 });
  });

  it("gives a CO2-goal portfolio its dimension with no compliance framing", () => {
    expect(
      selectGoalCallout({
        view: "current-stock",
        goal: "Reducing CO2 emissions",
        bandCounts,
      }),
    ).toEqual({ kind: "dimension-total", dimension: "carbon" });
  });

  it("falls back to the sector's below-C lens when the portfolio goal is None", () => {
    expect(
      selectGoalCallout({ view: "current-stock", goal: "None", bandCounts }),
    ).toEqual({ kind: "below-band", band: "C", count: 218 });
  });

  it("shows below-band movement for an EPC scenario, at the scenario's own target band", () => {
    expect(
      selectGoalCallout({
        view: "scenario",
        goal: "Increasing EPC",
        goalValue: "B",
        bandCounts,
        scenarioBandCounts: { B: 120, C: 150, D: 30, E: 12, Unknown: 6 },
      }),
    ).toEqual({
      kind: "below-band-movement",
      band: "B",
      before: 310, // C..G below B
      after: 192,
    });
  });

  it("shows dimension movement for a value-less scenario goal", () => {
    expect(
      selectGoalCallout({
        view: "scenario",
        goal: "Energy Savings",
        goalValue: null,
        bandCounts,
        scenarioBandCounts: {},
      }),
    ).toEqual({ kind: "dimension-movement", dimension: "energy" });
  });
});

describe("carsOffTheRoad", () => {
  it("converts annual tonnes saved into whole cars via the named factor", () => {
    expect(carsOffTheRoad(312)).toBe(
      Math.round(312 / TONNES_CO2_PER_CAR_PER_YEAR),
    );
    expect(Number.isInteger(carsOffTheRoad(312))).toBe(true);
  });

  it("reports zero cars for zero or negative savings", () => {
    expect(carsOffTheRoad(0)).toBe(0);
    expect(carsOffTheRoad(-5)).toBe(0);
  });
});

describe("shapeKpiDelta", () => {
  it("treats a drop as an improvement for lower-is-better metrics", () => {
    expect(
      shapeKpiDelta({ current: 1284, after: 941, improvesWhenLower: true }),
    ).toEqual({ delta: -343, improved: true });
  });

  it("treats a rise as an improvement for higher-is-better metrics", () => {
    expect(
      shapeKpiDelta({ current: 58, after: 71, improvesWhenLower: false }),
    ).toEqual({ delta: 13, improved: true });
  });

  it("reports no improvement when nothing moved", () => {
    expect(
      shapeKpiDelta({ current: 100, after: 100, improvesWhenLower: true }),
    ).toEqual({ delta: 0, improved: false });
  });
});

describe("buildHeadline", () => {
  it("tells an EPC scenario's story in one board-quotable sentence", () => {
    expect(
      buildHeadline({
        goal: "Increasing EPC",
        goalValue: "C",
        homesUpgraded: 218,
        netCost: 2_477_000,
        carbonSavedPerYear: 312,
      }),
    ).toBe(
      "Reaching EPC C across 218 homes costs £2.48m net and cuts carbon by 312 tonnes a year — like taking 173 cars off the road.",
    );
  });

  it("leads with carbon for a CO2 scenario", () => {
    expect(
      buildHeadline({
        goal: "Reducing CO2 emissions",
        goalValue: null,
        homesUpgraded: 246,
        netCost: 1_948_000,
        carbonSavedPerYear: 368,
      }),
    ).toBe(
      "Cutting carbon by 368 tonnes a year across 246 homes costs £1.95m net — like taking 204 cars off the road.",
    );
  });

  it("drops the car clause when no carbon is saved", () => {
    expect(
      buildHeadline({
        goal: "Valuation Improvement",
        goalValue: null,
        homesUpgraded: 187,
        netCost: 850_000,
        carbonSavedPerYear: 0,
      }),
    ).toBe("Improving valuation across 187 homes costs £850k net.");
  });
});
