import fetchMock from "@fetch-mock/jest";

import { EmptyAPIResponse } from "__fixtures__/Fetch";
import { inReactiveContext } from "__fixtures__/MobX";
import { MockGroup } from "__fixtures__/Stories";
import {
  AlertStore,
  AlertStoreStatuses,
  FormatAlertsQ,
  DecodeLocationSearch,
  UpdateLocationSearch,
  NewUnappliedFilter,
} from "Stores/AlertStore";

declare let global: typeof globalThis;

beforeEach(() => {
  fetchMock.mockReset();
});

afterEach(() => {
  jest.restoreAllMocks();
  fetchMock.mockReset();
});

describe("AlertStore.data", () => {
  it("getClusterAlertmanagersWithoutReadOnly filters out readonly instances", () => {
    const store = new AlertStore([]);
    store.data.setUpstreams({
      counters: { total: 2, healthy: 2, failed: 0 },
      clusters: { default: ["default", "readonly"] },
      instances: [
        {
          name: "default",
          uri: "http://localhost",
          publicURI: "http://example.com:8080",
          readonly: false,
          headers: { foo: "bar" },
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["default", "readonly"],
        },
        {
          name: "readonly",
          uri: "http://localhost:8081",
          publicURI: "http://example.com",
          readonly: true,
          headers: {},
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["default", "readonly"],
        },
      ],
    });
    expect(
      store.data.getClusterAlertmanagersWithoutReadOnly("default"),
    ).toEqual(["default"]);
  });

  it("getClusterAlertmanagersWithoutReadOnly handles clusters with no writable instances", () => {
    const store = new AlertStore([]);
    store.data.setUpstreams({
      counters: { total: 2, healthy: 2, failed: 0 },
      clusters: { default: ["ro1", "ro2"] },
      instances: [
        {
          name: "ro1",
          uri: "http://localhost",
          publicURI: "http://example.com:8080",
          readonly: true,
          headers: {},
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["ro1", "ro2"],
        },
        {
          name: "ro2",
          uri: "http://localhost:8081",
          publicURI: "http://example.com",
          readonly: true,
          headers: {},
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["ro1", "ro2"],
        },
      ],
    });
    expect(
      store.data.getClusterAlertmanagersWithoutReadOnly("default"),
    ).toEqual([]);
  });

  it("clustersWithoutReadOnly lists healthy members first", () => {
    const store = new AlertStore([]);
    store.data.setUpstreams({
      counters: { total: 2, healthy: 1, failed: 1 },
      clusters: { default: ["am1", "am2"] },
      instances: [
        {
          name: "am1",
          uri: "http://localhost",
          publicURI: "http://example.com:8080",
          readonly: false,
          headers: { foo: "bar" },
          corsCredentials: "include",
          error: "bad",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["am1", "am2"],
        },
        {
          name: "am2",
          uri: "http://localhost:8081",
          publicURI: "http://example.com",
          readonly: false,
          headers: {},
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["am1", "am2"],
        },
      ],
    });
    inReactiveContext(() => {
      expect(store.data.clustersWithoutReadOnly).toEqual({
        default: ["am2", "am1"],
      });
    });
  });

  it("clustersWithoutReadOnly keeps healthy order", () => {
    const store = new AlertStore([]);
    store.data.setUpstreams({
      counters: { total: 3, healthy: 3, failed: 0 },
      clusters: { default: ["am1", "am2", "am3"] },
      instances: [
        {
          name: "am1",
          uri: "http://localhost",
          publicURI: "http://example.com:8080",
          readonly: false,
          headers: { foo: "bar" },
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["am1", "am2", "am3"],
        },
        {
          name: "am2",
          uri: "http://localhost:8081",
          publicURI: "http://example.com",
          readonly: false,
          headers: {},
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["am1", "am2", "am3"],
        },
        {
          name: "am3",
          uri: "http://localhost:8081",
          publicURI: "http://example.com",
          readonly: false,
          headers: {},
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["am1", "am2", "am3"],
        },
      ],
    });
    inReactiveContext(() => {
      expect(store.data.clustersWithoutReadOnly).toEqual({
        default: ["am1", "am2", "am3"],
      });
    });
  });
});

describe("AlertStore.status", () => {
  it("status is initially idle with no error", () => {
    const store = new AlertStore([]);
    expect(store.status.value).toEqual(AlertStoreStatuses.Idle);
    expect(store.status.error).toBeNull();
  });

  it("status is Fetching with no error after setFetching()", () => {
    const store = new AlertStore([]);
    store.status.setFetching();
    expect(store.status.value).toEqual(AlertStoreStatuses.Fetching);
    expect(store.status.error).toBeNull();
  });

  it("status is Processing with no error after setProcessing()", () => {
    const store = new AlertStore([]);
    store.status.setProcessing();
    expect(store.status.value).toEqual(AlertStoreStatuses.Processing);
    expect(store.status.error).toBeNull();
  });

  it("status is Failure with correct error after setFailure", () => {
    const store = new AlertStore([]);
    store.status.setFailure("my error");
    expect(store.status.value).toEqual(AlertStoreStatuses.Failure);
    expect(store.status.error).toEqual("my error");
  });

  it("status is Idle with no error after setFetching and setIdle", () => {
    const store = new AlertStore([]);
    store.status.setFetching();
    store.status.setIdle();
    expect(store.status.value).toEqual(AlertStoreStatuses.Idle);
    expect(store.status.error).toBeNull();
  });

  it("status is Idle with no error after setFailure and setIdle", () => {
    const store = new AlertStore([]);
    store.status.setFailure("foo");
    store.status.setIdle();
    expect(store.status.value).toEqual(AlertStoreStatuses.Idle);
    expect(store.status.error).toBeNull();
  });

  it("togglePause() toggles pause state", () => {
    const store = new AlertStore([]);
    expect(store.status.paused).toBe(false);
    store.status.togglePause();
    expect(store.status.paused).toBe(true);
    store.status.togglePause();
    expect(store.status.paused).toBe(false);
  });

  it("togglePause() always leaves store paused if it's stopped", () => {
    const store = new AlertStore([]);
    expect(store.status.paused).toBe(false);
    store.status.stop();
    expect(store.status.paused).toBe(true);
    store.status.togglePause();
    expect(store.status.paused).toBe(true);
    store.status.togglePause();
    expect(store.status.paused).toBe(true);
  });

  it("stop() enforces a pause", () => {
    const store = new AlertStore([]);
    expect(store.status.paused).toBe(false);
    store.status.stop();
    expect(store.status.paused).toBe(true);
    store.status.resume();
    expect(store.status.paused).toBe(true);
  });
});

describe("AlertStore.info", () => {
  it("setUpgradeNeeded() sets upgradeNeeded to true", () => {
    const store = new AlertStore([]);
    expect(store.info.upgradeNeeded).toBe(false);
    store.info.setUpgradeNeeded(true);
    expect(store.info.upgradeNeeded).toBe(true);
    store.info.setUpgradeNeeded(true);
    expect(store.info.upgradeNeeded).toBe(true);
  });
});

describe("AlertStore.filters", () => {
  it("addFilter('foo') should create a correct empty filter", () => {
    const store = new AlertStore([]);
    store.filters.addFilter("foo");
    expect(store.filters.values).toHaveLength(1);
    expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("foo"));
  });

  it("addFilter should not allow duplicates", () => {
    const store = new AlertStore([]);
    store.filters.addFilter("foo");
    store.filters.addFilter("foo");
    expect(store.filters.values).toHaveLength(1);
  });

  it("removeFilter('foo') should remove passed filter if it's defined", () => {
    const store = new AlertStore([]);
    store.filters.addFilter("foo");
    store.filters.removeFilter("foo");
    expect(store.filters.values).toHaveLength(0);
  });

  it("removeFilter('foo') should not remove filters other than 'foo'", () => {
    const store = new AlertStore([]);
    store.filters.addFilter("bar");
    store.filters.addFilter("foo");
    store.filters.addFilter("baz");
    store.filters.removeFilter("foo");
    expect(store.filters.values).toHaveLength(2);
    expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("bar"));
    expect(store.filters.values[1]).toMatchObject(NewUnappliedFilter("baz"));
  });

  it("removeFilter('foo') should not remove any filter if 'foo' isn't defined", () => {
    const store = new AlertStore([]);
    store.filters.addFilter("bar");
    store.filters.removeFilter("foo");
    expect(store.filters.values).toHaveLength(1);
    expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("bar"));
  });

  it("replaceFilter('foo', 'bar') should add 'bar' in empty list", () => {
    const store = new AlertStore([]);
    store.filters.replaceFilter("foo", "bar");
    expect(store.filters.values).toHaveLength(1);
  });

  it("replaceFilter('foo', 'new') should replace correct filter", () => {
    const store = new AlertStore([]);
    store.filters.addFilter("bar");
    store.filters.addFilter("foo");
    store.filters.addFilter("baz");
    store.filters.replaceFilter("foo", "new");
    expect(store.filters.values).toHaveLength(3);
    expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("bar"));
    expect(store.filters.values[1]).toMatchObject(NewUnappliedFilter("new"));
    expect(store.filters.values[2]).toMatchObject(NewUnappliedFilter("baz"));
  });

  it("replaceFilter('foo', 'bar') should not allow duplicates", () => {
    const store = new AlertStore([]);
    store.filters.addFilter("foo");
    store.filters.addFilter("bar");
    store.filters.replaceFilter("foo", "bar");
    expect(store.filters.values).toHaveLength(1);
    expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("bar"));
  });

  it("addFilter() updates window.history", () => {
    const store = new AlertStore([]);
    const historyMock = jest.spyOn(global.window.history, "pushState");
    store.filters.addFilter("foo");
    expect(historyMock).toHaveBeenLastCalledWith(
      null,
      "",
      "http://localhost/?q=foo",
    );
  });

  it("replaceFilter() updates window.history", () => {
    const store = new AlertStore(["foo"]);
    const historyMock = jest.spyOn(global.window.history, "pushState");
    store.filters.replaceFilter("foo", "bar");
    expect(historyMock).toHaveBeenLastCalledWith(
      null,
      "",
      "http://localhost/?q=bar",
    );
  });

  it("setFilters() updates window.history", () => {
    const store = new AlertStore([]);
    store.filters.addFilter("foo");
    store.filters.addFilter("bar");

    const historyMock = jest.spyOn(global.window.history, "pushState");
    store.filters.setFilters(["baz", "far"]);
    expect(store.filters.values).toHaveLength(2);
    expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("baz"));
    expect(store.filters.values[1]).toMatchObject(NewUnappliedFilter("far"));
    expect(historyMock).toHaveBeenLastCalledWith(
      null,
      "",
      "http://localhost/?q=baz&q=far",
    );
  });

  it("setWithoutLocation() doesn't update window.history", () => {
    const store = new AlertStore(["far", "foo"]);

    const historyMock = jest.spyOn(global.window.history, "pushState");
    store.filters.setWithoutLocation(["baz", "far"]);
    expect(store.filters.values).toHaveLength(2);
    expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("baz"));
    expect(store.filters.values[1]).toMatchObject(NewUnappliedFilter("far"));
    expect(historyMock).not.toHaveBeenCalled();
  });

  it("setWithoutLocation() adds missing filters", () => {
    const store = new AlertStore([]);
    store.filters.setWithoutLocation(["foo", "bar"]);
    expect(store.filters.values).toHaveLength(2);
    expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("foo"));
    expect(store.filters.values[1]).toMatchObject(NewUnappliedFilter("bar"));
  });

  it("setWithoutLocation() removes orphaned filters", () => {
    const store = new AlertStore(["far"]);
    store.filters.setWithoutLocation([]);
    expect(store.filters.values).toHaveLength(0);
  });
});

describe("FormatAlertsQ", () => {
  it("encodes multiple values without indices", () => {
    expect(FormatAlertsQ(["a", "b"])).toBe("q=a&q=b");
  });
});

describe("DecodeLocationSearch", () => {
  const defaultParams = {
    defaultsUsed: true,
    params: { q: [] },
  };

  it("empty ('') search param is decoded correctly", () => {
    expect(DecodeLocationSearch("")).toMatchObject(defaultParams);
  });

  it("empty ('?') search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?")).toMatchObject(defaultParams);
  });

  it("no value q[]= search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?q[]=")).toMatchObject({
      defaultsUsed: false,
      params: { q: [] },
    });
  });

  it("no value q= search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?q=")).toMatchObject({
      defaultsUsed: false,
      params: { q: [] },
    });
  });

  it("no value q[]=&q[]= search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?q=")).toMatchObject({
      defaultsUsed: false,
      params: { q: [] },
    });
  });

  it("single value q=foo search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?q=foo")).toMatchObject({
      defaultsUsed: false,
      params: { q: ["foo"] },
    });
  });

  it("single value q[]=foo search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?q[]=foo")).toMatchObject({
      defaultsUsed: false,
      params: { q: ["foo"] },
    });
  });

  it("multi value q[]=foo&q[]=bar search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?q[]=foo&q[]=bar")).toMatchObject({
      defaultsUsed: false,
      params: { q: ["foo", "bar"] },
    });
  });

  it("multi value q[]=foo&q[]=bar&q[]=foo search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?q[]=foo&q[]=bar&q[]=foo")).toMatchObject({
      defaultsUsed: false,
      params: { q: ["foo", "bar"] },
    });
  });

  it("multi value q[]=foo&q[]=&q[]=foo search param is decoded correctly", () => {
    expect(DecodeLocationSearch("?q[]=foo&q[]=&q[]=foo")).toMatchObject({
      defaultsUsed: false,
      params: { q: ["foo"] },
    });
  });
});

describe("UpdateLocationSearch", () => {
  it("{q: foo} is pushed to location.search", () => {
    UpdateLocationSearch({ q: ["foo"] });
    expect(window.location.search).toBe("?q=foo");
  });

  it("{a: foo} is not pushed to location.search", () => {
    UpdateLocationSearch({ a: "foo" } as unknown as Parameters<
      typeof UpdateLocationSearch
    >[0]);
    expect(window.location.search).toBe("?q=");
  });

  it("{a: foo, q: bar} is pushed to location.search", () => {
    UpdateLocationSearch({ a: "foo", q: ["bar"] } as unknown as Parameters<
      typeof UpdateLocationSearch
    >[0]);
    expect(window.location.search).toBe("?q=bar");
  });

  it("{q: [1, 2]} is pushed to location.search", () => {
    UpdateLocationSearch({ q: ["1", "2"] });
    expect(window.location.search).toBe("?q=1&q=2");
  });
});

describe("AlertStore.fetch", () => {
  it("parseAPIResponse() rejects a response with mismatched filters", () => {
    const consoleSpy = jest.spyOn(console, "info").mockImplementation(() => {});

    const response = EmptyAPIResponse();
    const store = new AlertStore([]);
    store.parseAPIResponse(response);

    expect(store.status.value).toEqual(AlertStoreStatuses.Idle);
    // there should be no filters set on AlertStore instance since we started
    // with 0 and rejected response with 1 filter
    expect(store.filters.values).toHaveLength(0);
    // console.info should have been called since we emitted a log line
    expect(consoleSpy).toHaveBeenCalledTimes(1);
  });

  it("parseAPIResponse() works for a single filter 'label=value'", () => {
    const response = EmptyAPIResponse();

    const store = new AlertStore(["label=value"]);
    store.parseAPIResponse(response);

    expect(store.status.value).toEqual(AlertStoreStatuses.Idle);
    expect(store.info.version).toBe("fakeVersion");
    expect(store.filters.values[0].applied).toBe(true);
  });

  it("fetch() works with valid response", async () => {
    const response = EmptyAPIResponse();
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });

    const store = new AlertStore(["label=value"]);
    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toBeUndefined();

    expect(fetchMock.callHistory.calls()).toHaveLength(1);
    expect(store.status.value).toEqual(AlertStoreStatuses.Idle);
    expect(store.info.version).toBe("fakeVersion");
  });

  it("fetch() handles response with error correctly", async () => {
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify({ error: "Fetch error" }),
    });

    const store = new AlertStore([]);
    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toBeUndefined();

    expect(fetchMock.callHistory.calls()).toHaveLength(1);
    expect(store.status.value).toEqual(AlertStoreStatuses.Failure);
    expect(store.info.version).toBe("unknown");
  });

  it("fetch() handles response that throws an error correctly", async () => {
    const consoleSpy = jest
      .spyOn(console, "trace")
      .mockImplementation(() => {});
    fetchMock.mockReset();
    fetchMock.route("*", {
      throws: new Error("fetch error"),
    });

    const store = new AlertStore([]);
    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toHaveProperty("error");

    expect(fetchMock.callHistory.calls()).toHaveLength(10);
    expect(store.status.value).toEqual(AlertStoreStatuses.Failure);
    expect(store.info.version).toBe("unknown");
    // there should be a trace of the error
    expect(consoleSpy).toHaveBeenCalledTimes(1);
  });

  it("fetch() retries on failure", async () => {
    jest.spyOn(console, "trace").mockImplementation(() => {});
    const store = new AlertStore([]);

    fetchMock.mockReset();
    fetchMock.route("*", {
      throws: new Error("fetch error"),
    });

    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toHaveProperty("error");
    expect(fetchMock.callHistory.calls()).toHaveLength(10);
  });

  it("fetch() retry counter is reset after successful fetch", async () => {
    jest.spyOn(console, "trace").mockImplementation(() => {});
    const store = new AlertStore(["label=value"]);

    fetchMock.mockReset();
    fetchMock.route("*", {
      throws: new Error("fetch error"),
    });

    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toHaveProperty("error");
    expect(fetchMock.callHistory.calls()).toHaveLength(10);

    const response = EmptyAPIResponse();
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });

    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toBeUndefined();
    expect(fetchMock.callHistory.calls()).toHaveLength(1);

    fetchMock.mockReset();
    fetchMock.route("*", {
      throws: new Error("fetch error"),
    });

    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toHaveProperty("error");
    expect(fetchMock.callHistory.calls()).toHaveLength(10);
  });

  it("fetch() reloads the page after if auth middleware is detected", async () => {
    jest.spyOn(console, "trace").mockImplementation(() => {});

    const store = new AlertStore(["label=value"]);

    jest.spyOn(global, "fetch").mockImplementation(
      async () =>
        Promise.resolve({
          type: "opaque",
          body: "auth needed",
          json: jest.fn(() => EmptyAPIResponse()),
        }) as unknown as Promise<Response>,
    );

    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toBeUndefined();

    expect(store.info.reloadNeeded).toBe(true);
  });

  it("unapplied filters are marked as applied on fetch error", async () => {
    const store = new AlertStore([]);
    store.filters.setFilterValues([NewUnappliedFilter("foo")]);

    jest.spyOn(console, "trace").mockImplementation(() => {});
    fetchMock.mockReset();
    fetchMock.route("*", {
      throws: new Error("fetch error"),
    });

    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toHaveProperty("error");
    expect(store.filters.values[0].applied).toBe(true);
  });

  it("stored settings are updated if needed after fetch", async () => {
    const response = EmptyAPIResponse();
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });

    const store = new AlertStore(["label=value"]);

    // initial fetch, should update settings
    store.settings.setValues({ foo: "bar" } as unknown as Parameters<
      typeof store.settings.setValues
    >[0]);
    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toBeUndefined();
    expect(store.settings.values).toMatchObject({
      labels: {
        job: {
          isStatic: true,
          isValueOnly: false,
        },
      },
      annotationsDefaultHidden: false,
      annotationsHidden: [],
      annotationsVisible: [],
      annotationsEnableHTML: false,
    });

    // second fetch, should keep same settings
    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toBeUndefined();
    expect(store.settings.values).toMatchObject({
      labels: {
        job: {
          isStatic: true,
          isValueOnly: false,
        },
      },
      annotationsDefaultHidden: false,
      annotationsHidden: [],
      annotationsVisible: [],
      annotationsEnableHTML: false,
    });
  });

  it("wants to reload page after new version is returned in the API", async () => {
    const response = EmptyAPIResponse();
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });
    const store = new AlertStore(["label=value"]);
    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toBeUndefined();
    expect(store.info.upgradeReady).toBe(false);

    response.version = "newFakeVersion";
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });
    await expect(
      store.fetch("", false, "", "", false, {}, 5, {}),
    ).resolves.toBeUndefined();
    expect(store.info.upgradeReady).toBe(true);
  });

  it("adds new groups to the store after fetch", () => {
    const response = EmptyAPIResponse();
    const g1 = MockGroup("group1", 1, 1, 0);
    const g2 = MockGroup("group2", 1, 1, 0);
    response.grids = [
      {
        labelName: "",
        labelValue: "",
        alertGroups: [g1, g2],
        totalGroups: 2,
        stateCount: { unprocessed: 0, active: 2, suppressed: 0 },
      },
    ];
    const store = new AlertStore(["label=value"]);
    store.parseAPIResponse(response);
    expect(store.data.grids).toHaveLength(1);
    expect(Object.keys(store.data.grids[0].alertGroups)).toHaveLength(2);
    expect(store.data.grids[0].alertGroups).toMatchObject([g1, g2]);
  });

  it("removes old groups from the store after fetch", () => {
    const store = new AlertStore(["label=value"]);
    const g1 = MockGroup("group1", 1, 1, 0);
    const g2 = MockGroup("group2", 1, 1, 0);
    const g3 = MockGroup("group3", 1, 1, 0);
    store.data.setGrids([
      {
        labelName: "",
        labelValue: "",
        alertGroups: [g1, g2, g3],
        totalGroups: 3,
        stateCount: { unprocessed: 0, active: 3, suppressed: 0 },
      },
    ]);
    expect(store.data.grids).toHaveLength(1);
    expect(Object.keys(store.data.grids[0].alertGroups)).toHaveLength(3);

    const response = EmptyAPIResponse();
    response.grids = [
      {
        labelName: "",
        labelValue: "",
        alertGroups: [g1, g3],
        totalGroups: 2,
        stateCount: { unprocessed: 0, active: 2, suppressed: 0 },
      },
    ];

    store.parseAPIResponse(response);
    expect(Object.keys(store.data.grids[0].alertGroups)).toHaveLength(2);
    expect(store.data.grids[0].alertGroups).toMatchObject([g1, g3]);
  });

  it("uses correct query args with gridSortReverse=false", async () => {
    const response = EmptyAPIResponse();
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });
    const store = new AlertStore(["label=value"]);
    await expect(
      store.fetch("", false, "sortOrder", "sortLabel", false, {}, 5, {}),
    ).resolves.toBeUndefined();
    expect(fetchMock.callHistory.calls().length).toEqual(1);
    expect(fetchMock.callHistory.calls()[0]?.url).toContain("/alerts.json");
    expect(
      JSON.parse(fetchMock.callHistory.calls()[0]?.options?.body as string),
    ).toStrictEqual({
      filters: ["label=value"],
      gridLabel: "",
      gridLimits: {},
      gridSortReverse: false,
      sortOrder: "sortOrder",
      sortLabel: "sortLabel",
      sortReverse: false,
      defaultGroupLimit: 5,
      groupLimits: {},
    });
  });

  it("uses correct query args with gridSortReverse=true", async () => {
    const response = EmptyAPIResponse();
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });
    const store = new AlertStore(["label=value"]);
    await expect(
      store.fetch("cluster", true, "sortOrder", "sortLabel", true, {}, 5, {}),
    ).resolves.toBeUndefined();
    expect(fetchMock.callHistory.calls().length).toEqual(1);
    expect(fetchMock.callHistory.calls()[0]?.url).toContain("/alerts.json");
    expect(
      JSON.parse(fetchMock.callHistory.calls()[0]?.options?.body as string),
    ).toStrictEqual({
      filters: ["label=value"],
      gridLabel: "cluster",
      gridLimits: {},
      gridSortReverse: true,
      sortOrder: "sortOrder",
      sortLabel: "sortLabel",
      sortReverse: true,
      defaultGroupLimit: 5,
      groupLimits: {},
    });
  });

  it("uses correct query args with limits", async () => {
    const response = EmptyAPIResponse();
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });
    const store = new AlertStore(["label=value"]);
    store.ui.setGridGroupLimit("foo", "bar", 7);
    store.ui.setGroupAlertLimit("1234567890", 7);
    store.ui.setGroupAlertLimit("1234567891", 1);

    await expect(
      store.fetch(
        "cluster",
        true,
        "sortOrder",
        "sortLabel",
        false,
        {
          bar: 7,
        },
        5,
        { "1234567890": 7, "1234567891": 1 },
      ),
    ).resolves.toBeUndefined();
    expect(fetchMock.callHistory.calls().length).toEqual(1);
    expect(fetchMock.callHistory.calls()[0]?.url).toContain("/alerts.json");
    expect(
      JSON.parse(fetchMock.callHistory.calls()[0]?.options?.body as string),
    ).toStrictEqual({
      filters: ["label=value"],
      gridLabel: "cluster",
      gridLimits: { bar: 7 },
      gridSortReverse: true,
      sortOrder: "sortOrder",
      sortLabel: "sortLabel",
      sortReverse: false,
      defaultGroupLimit: 5,
      groupLimits: { "1234567890": 7, "1234567891": 1 },
    });
  });

  it("purges unknown group limits after fetch", async () => {
    const store = new AlertStore(["label=value"]);
    store.ui.setGridGroupLimit("foo", "bar", 7);
    store.ui.setGroupAlertLimit("g1", 7);
    store.ui.setGroupAlertLimit("g2", 1);
    store.ui.setGroupAlertLimit("g4", 6);

    const g1 = MockGroup("group1", 1, 1, 0);
    g1.id = "g1";
    const g2 = MockGroup("group2", 1, 1, 0);
    g2.id = "g2";
    const g3 = MockGroup("group3", 1, 1, 0);
    g3.id = "g3";
    const response = EmptyAPIResponse();
    response.grids = [
      {
        labelName: "",
        labelValue: "",
        alertGroups: [g1, g2, g3],
        totalGroups: 3,
        stateCount: { unprocessed: 0, active: 3, suppressed: 0 },
      },
    ];
    fetchMock.mockReset();
    fetchMock.route("*", {
      body: JSON.stringify(response),
    });

    await expect(
      store.fetch(
        "cluster",
        true,
        "sortOrder",
        "sortLabel",
        false,
        {
          bar: 7,
        },
        5,
        { g1: 7, g2: 1, g4: 6 },
      ),
    ).resolves.toBeUndefined();
    expect(fetchMock.callHistory.calls().length).toEqual(1);
    expect(fetchMock.callHistory.calls()[0]?.url).toContain("/alerts.json");
    expect(
      JSON.parse(fetchMock.callHistory.calls()[0]?.options?.body as string),
    ).toStrictEqual({
      filters: ["label=value"],
      gridLabel: "cluster",
      gridLimits: { bar: 7 },
      gridSortReverse: true,
      sortOrder: "sortOrder",
      sortLabel: "sortLabel",
      sortReverse: false,
      defaultGroupLimit: 5,
      groupLimits: { g1: 7, g2: 1, g4: 6 },
    });
    expect(store.ui.groupAlertLimits).toStrictEqual({ g1: 7, g2: 1 });
  });
});
