import { describe, expect, it } from "vitest";
import {
  buildRunRecord,
  deriveRunStatus,
  estimateRunMinutes,
  historyRefetchInterval,
  isLargeRun,
  normaliseRunFilters,
  parseRunConfig,
  selectionSummary,
  type RunStatus,
} from "./model";

describe("normaliseRunFilters", () => {
  it("canonicalises postcodes, dedupes, and drops empty selections (absent = all)", () => {
    const out = normaliseRunFilters({
      postcodes: ["m20  4tf", "M20 4TF", "b93 8su"],
      propertyTypes: [],
      builtForms: undefined,
    });
    expect(out).toEqual({
      ok: true,
      filters: { postcodes: ["B93 8SU", "M20 4TF"] },
    });
  });

  it("accepts canonical types/built forms plus Unknown, rejects anything else", () => {
    const ok = normaliseRunFilters({
      propertyTypes: ["House", "Unknown"],
      builtForms: ["Semi-Detached"],
    });
    expect(ok).toEqual({
      ok: true,
      filters: { propertyTypes: ["House", "Unknown"], builtForms: ["Semi-Detached"] },
    });
    expect(normaliseRunFilters({ propertyTypes: ["Castle"] }).ok).toBe(false);
    expect(normaliseRunFilters({ builtForms: ["Terraced"] }).ok).toBe(false);
  });

  it("caps the postcode selection at 40 — 'all postcodes' is absence, not enumeration", () => {
    const many = Array.from({ length: 41 }, (_, i) => `M${Math.floor(i / 10)}${i % 10} 4TF`);
    const out = normaliseRunFilters({ postcodes: many });
    expect(out.ok).toBe(false);
    if (!out.ok) expect(out.error).toMatch(/40/);
    expect(normaliseRunFilters({ postcodes: many.slice(0, 40) }).ok).toBe(true);
  });
});

describe("normaliseRunFilters — tags", () => {
  it("normalises tag ids to a deduped, ascending number list", () => {
    expect(normaliseRunFilters({ tagIds: [3, "1", 3, 2] })).toEqual({
      ok: true,
      filters: { tagIds: [1, 2, 3] },
    });
  });

  it("rejects a non-numeric or non-positive tag id", () => {
    expect(normaliseRunFilters({ tagIds: [1, "x"] }).ok).toBe(false);
    expect(normaliseRunFilters({ tagIds: [0] }).ok).toBe(false);
  });

  it("treats an absent/empty tag selection as unconstrained (no key)", () => {
    expect(normaliseRunFilters({ tagIds: [] })).toEqual({ ok: true, filters: {} });
  });
});

describe("buildRunRecord", () => {
  it("produces the app-authored task row carrying the run config as its inputs (ADR-0008)", () => {
    const record = buildRunRecord({
      portfolioId: "814",
      scenarioIds: ["12", "34"],
      filters: { postcodes: ["B93 8SU"], propertyTypes: ["House"] },
      previewedPropertyCount: 214,
      triggeredBy: "khalim@domna.homes",
    });
    expect(record.task).toEqual({
      taskSource: "Modelling run – 2 scenarios",
      service: "modelling_run",
      source: "portfolio_id",
      sourceId: "814",
      status: "waiting",
      inputs: JSON.stringify({
        portfolio_id: 814,
        scenario_ids: [12, 34],
        filters: { postcodes: ["B93 8SU"], property_types: ["House"] },
        previewed_property_count: 214,
        triggered_by: "khalim@domna.homes",
      }),
    });
    // What the distributor receives — provenance fields stay on the task only
    expect(record.dispatchPayload).toEqual({
      portfolio_id: 814,
      scenario_ids: [12, 34],
      filters: { postcodes: ["B93 8SU"], property_types: ["House"] },
    });
  });

  it("carries tag ids into the task inputs, the dispatch payload, and back (ADR-0013)", () => {
    const { task, dispatchPayload } = buildRunRecord({
      portfolioId: "5",
      scenarioIds: ["2"],
      filters: { tagIds: [7, 3] },
      previewedPropertyCount: 10,
      triggeredBy: "u@x.io",
    });
    expect(dispatchPayload.filters).toEqual({ tag_ids: [7, 3] });
    expect(parseRunConfig(task.inputs)?.filters.tagIds).toEqual([7, 3]);
  });
});

describe("parseRunConfig", () => {
  it("round-trips a stored run record back to the app's shape", () => {
    const { task } = buildRunRecord({
      portfolioId: "814",
      scenarioIds: ["12"],
      filters: { builtForms: ["Detached", "Unknown"] },
      previewedPropertyCount: 57,
      triggeredBy: "priya@domna.homes",
    });
    expect(parseRunConfig(task.inputs)).toEqual({
      scenarioIds: ["12"],
      filters: { builtForms: ["Detached", "Unknown"] },
      previewedPropertyCount: 57,
      triggeredBy: "priya@domna.homes",
    });
  });

  it("returns null for missing or malformed inputs — never throws on old rows", () => {
    expect(parseRunConfig(null)).toBeNull();
    expect(parseRunConfig("not json")).toBeNull();
    expect(parseRunConfig('{"unrelated":true}')).toBeNull();
  });
});

describe("selectionSummary", () => {
  it("describes the filters in the run-history format; no filters = All properties", () => {
    expect(selectionSummary({})).toBe("All properties");
    expect(
      selectionSummary({
        postcodes: ["B93 8SU", "M20 4TF"],
        propertyTypes: ["House"],
        builtForms: ["Detached", "Unknown"],
      }),
    ).toBe("Postcodes: B93 8SU, M20 4TF · Type: House · Built form: Detached, Unknown");
  });

  it("summarises a tag selection as a count (names aren't in the pure filters)", () => {
    expect(selectionSummary({ tagIds: [7, 3] })).toBe("Tags: 2");
    expect(selectionSummary({ tagIds: [7] })).toBe("Tags: 1");
  });
});

describe("deriveRunStatus", () => {
  const base = { taskStatus: "waiting", jobCompleted: null, total: 0, completed: 0, failed: 0 };
  it("maps the task lifecycle to the four display states", () => {
    // dispatched: accepted, no execution subtasks yet
    expect(deriveRunStatus(base)).toEqual({ state: "dispatched" });
    // in progress once execution subtasks exist, with progress
    expect(
      deriveRunStatus({ ...base, taskStatus: "in progress", total: 104, completed: 61 }),
    ).toEqual({ state: "in_progress", ready: 61, total: 104 });
    // complete when the task says so
    expect(
      deriveRunStatus({ ...base, taskStatus: "completed", jobCompleted: new Date(), total: 104, completed: 104 }),
    ).toEqual({ state: "complete" });
    // failed statuses win regardless of counts
    expect(
      deriveRunStatus({ ...base, taskStatus: "Failed", total: 104, completed: 40, failed: 3 }),
    ).toEqual({ state: "failed" });
  });
});

describe("isLargeRun", () => {
  it("gates runs of 10,000+ matched properties behind an extra confirm", () => {
    expect(isLargeRun(9999)).toBe(false);
    expect(isLargeRun(10000)).toBe(true);
  });
});

describe("estimateRunMinutes", () => {
  it("scales with plan count — roughly 300 plans per 5 minutes", () => {
    expect(estimateRunMinutes(3_000)).toBe(50);
    expect(estimateRunMinutes(12_000)).toBe(200);
  });

  it("never promises less than 5 minutes, even for tiny or zero-plan runs", () => {
    expect(estimateRunMinutes(0)).toBe(5);
    expect(estimateRunMinutes(1)).toBe(5);
    expect(estimateRunMinutes(300)).toBe(5);
  });
});

describe("historyRefetchInterval", () => {
  const runs = (...states: RunStatus[]) => states.map((status) => ({ status }));

  it("polls fast while any run is still moving (dispatched or in progress)", () => {
    expect(historyRefetchInterval(runs({ state: "complete" }, { state: "dispatched" }))).toBe(
      5_000,
    );
    expect(
      historyRefetchInterval(runs({ state: "in_progress", ready: 1, total: 4 })),
    ).toBe(5_000);
  });

  it("settles to a slow poll when every run is finished, or history is unknown", () => {
    expect(historyRefetchInterval(runs({ state: "complete" }, { state: "failed" }))).toBe(30_000);
    expect(historyRefetchInterval([])).toBe(30_000);
    expect(historyRefetchInterval(undefined)).toBe(30_000);
  });
});
