import { describe, it, expect } from "vitest";
import {
  parseReportingViewState,
  serializeReportingViewState,
  serializeTagFilter,
  isTagFilterActive,
  tagFiltersEqual,
  emptyViewState,
  emptyTagFilter,
  type ReportingViewState,
} from "./viewState";

/** URLSearchParams from a query string, the shape the client/server hand in. */
const params = (qs: string) => new URLSearchParams(qs);

describe("parseReportingViewState — defaults", () => {
  it("returns the empty/default state for no params", () => {
    expect(parseReportingViewState(params(""))).toEqual(emptyViewState());
  });

  it("defaults view to current-stock, filters off, no tags", () => {
    const s = parseReportingViewState(params(""));
    expect(s.view).toBe("current-stock");
    expect(s.hideNonCompliant).toBe(false);
    expect(s.useLodgedBaseline).toBe(false);
    expect(s.complianceWindow).toBeNull();
    expect(s.tags).toEqual({ include: [], exclude: [], includeMode: "any" });
  });
});

describe("parseReportingViewState — view", () => {
  it("reads the recommended pseudo-scenario", () => {
    expect(parseReportingViewState(params("view=recommended")).view).toBe(
      "recommended",
    );
  });

  it("reads a numeric scenario id as a number", () => {
    expect(parseReportingViewState(params("view=1292")).view).toBe(1292);
  });

  it("falls back to current-stock for a nonsense view", () => {
    expect(parseReportingViewState(params("view=banana")).view).toBe(
      "current-stock",
    );
  });
});

describe("parseReportingViewState — scenario filters", () => {
  it("reads the boolean toggles", () => {
    const s = parseReportingViewState(
      params("hideNonCompliant=true&useLodgedBaseline=true"),
    );
    expect(s.hideNonCompliant).toBe(true);
    expect(s.useLodgedBaseline).toBe(true);
  });

  it("reads a valid compliance window", () => {
    const s = parseReportingViewState(
      params("complianceBand=C&complianceDate=2030-01-01"),
    );
    expect(s.complianceWindow).toEqual({ band: "C", date: "2030-01-01" });
  });

  it("ignores a compliance window with an out-of-range band", () => {
    expect(
      parseReportingViewState(
        params("complianceBand=Z&complianceDate=2030-01-01"),
      ).complianceWindow,
    ).toBeNull();
  });

  it("ignores a compliance window with a malformed date", () => {
    expect(
      parseReportingViewState(params("complianceBand=C&complianceDate=soon"))
        .complianceWindow,
    ).toBeNull();
  });

  it("needs both band and date for a compliance window", () => {
    expect(
      parseReportingViewState(params("complianceBand=C")).complianceWindow,
    ).toBeNull();
  });
});

describe("parseReportingViewState — tag filter", () => {
  it("parses comma-separated include and exclude ids", () => {
    const s = parseReportingViewState(
      params("includeTags=10,20&excludeTags=30"),
    );
    expect(s.tags.include).toEqual(["10", "20"]);
    expect(s.tags.exclude).toEqual(["30"]);
  });

  it("dedupes repeated ids, preserving first-seen order", () => {
    expect(
      parseReportingViewState(params("includeTags=10,10,20,10")).tags.include,
    ).toEqual(["10", "20"]);
  });

  it("drops non-numeric ids", () => {
    expect(
      parseReportingViewState(params("includeTags=10,abc,,20")).tags.include,
    ).toEqual(["10", "20"]);
  });

  it("resolves include/exclude overlap in favour of exclude", () => {
    const s = parseReportingViewState(
      params("includeTags=10,20&excludeTags=20,30"),
    );
    expect(s.tags.include).toEqual(["10"]);
    expect(s.tags.exclude).toEqual(["20", "30"]);
  });

  it("reads includeMode=all, defaulting to any otherwise", () => {
    expect(
      parseReportingViewState(params("includeTags=10&includeMode=all")).tags
        .includeMode,
    ).toBe("all");
    expect(
      parseReportingViewState(params("includeTags=10&includeMode=bogus")).tags
        .includeMode,
    ).toBe("any");
  });
});

describe("isTagFilterActive", () => {
  it("is false with no tags", () => {
    expect(isTagFilterActive(emptyViewState().tags)).toBe(false);
  });
  it("is true with an include", () => {
    expect(
      isTagFilterActive({ include: ["1"], exclude: [], includeMode: "any" }),
    ).toBe(true);
  });
  it("is true with an exclude", () => {
    expect(
      isTagFilterActive({ include: [], exclude: ["1"], includeMode: "any" }),
    ).toBe(true);
  });
});

describe("tagFiltersEqual", () => {
  it("treats two empty filters as equal", () => {
    expect(tagFiltersEqual(emptyTagFilter(), emptyTagFilter())).toBe(true);
  });

  it("is order-insensitive within include/exclude", () => {
    expect(
      tagFiltersEqual(
        { include: ["1", "2"], exclude: ["9"], includeMode: "any" },
        { include: ["2", "1"], exclude: ["9"], includeMode: "any" },
      ),
    ).toBe(true);
  });

  it("distinguishes a changed include set", () => {
    expect(
      tagFiltersEqual(
        { include: ["1"], exclude: [], includeMode: "any" },
        { include: ["1", "2"], exclude: [], includeMode: "any" },
      ),
    ).toBe(false);
  });

  it("distinguishes a changed exclude set", () => {
    expect(
      tagFiltersEqual(
        { include: [], exclude: ["1"], includeMode: "any" },
        { include: [], exclude: ["2"], includeMode: "any" },
      ),
    ).toBe(false);
  });

  it("distinguishes includeMode, but only when it bites (2+ includes)", () => {
    // With 2+ include tags, any vs all changes the query → not equal.
    expect(
      tagFiltersEqual(
        { include: ["1", "2"], exclude: [], includeMode: "any" },
        { include: ["1", "2"], exclude: [], includeMode: "all" },
      ),
    ).toBe(false);
    // With <2 include tags the mode is inert, so it must not force inequality
    // (else a stray mode flip would trigger a needless server round-trip).
    expect(
      tagFiltersEqual(
        { include: ["1"], exclude: [], includeMode: "any" },
        { include: ["1"], exclude: [], includeMode: "all" },
      ),
    ).toBe(true);
  });
});

describe("serializeTagFilter", () => {
  it("is empty for an empty filter", () => {
    expect(serializeTagFilter(emptyViewState().tags).toString()).toBe("");
  });
  it("emits include + exclude, and includeMode only when 'all' with 2+", () => {
    expect(
      serializeTagFilter({
        include: ["1", "2"],
        exclude: ["9"],
        includeMode: "all",
      }).toString(),
    ).toBe("includeTags=1%2C2&excludeTags=9&includeMode=all");
    expect(
      serializeTagFilter({
        include: ["1"],
        exclude: [],
        includeMode: "all",
      }).toString(),
    ).toBe("includeTags=1");
  });
});

describe("serializeReportingViewState — round-trip + clean URLs", () => {
  it("omits every default (empty query string)", () => {
    expect(serializeReportingViewState(emptyViewState()).toString()).toBe("");
  });

  it("omits includeMode when it is the default 'any'", () => {
    const qs = serializeReportingViewState({
      ...emptyViewState(),
      tags: { include: ["10"], exclude: [], includeMode: "any" },
    }).toString();
    expect(qs).toContain("includeTags=10");
    expect(qs).not.toContain("includeMode");
  });

  it("emits includeMode only when 'all' and there are 2+ include tags", () => {
    const qs = serializeReportingViewState({
      ...emptyViewState(),
      tags: { include: ["10", "20"], exclude: [], includeMode: "all" },
    }).toString();
    expect(qs).toContain("includeMode=all");
  });

  it("round-trips a fully-populated state", () => {
    const state: ReportingViewState = {
      view: 1292,
      hideNonCompliant: true,
      useLodgedBaseline: false,
      complianceWindow: { band: "C", date: "2030-01-01" },
      tags: { include: ["10", "20"], exclude: ["30"], includeMode: "all" },
    };
    const round = parseReportingViewState(serializeReportingViewState(state));
    expect(round).toEqual(state);
  });
});
