import { act } from "react";

import { render, fireEvent } from "@testing-library/react";

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

import { MockAlert, MockAlertGroup } from "__fixtures__/Alerts";
import {
  MockThemeContext,
  MockThemeContextWithoutAnimations,
} from "__fixtures__/Theme";
import { RainbowHistoryResponse } from "__fixtures__/AlertHistory";
import type { APIAlertGroupT, APIGridT } from "Models/APITypes";
import { AlertStore } from "Stores/AlertStore";
import { Settings, CollapseStateT } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { ThemeContext, ThemeCtx } from "Components/Theme";
import AlertGroup from ".";

let alertStore: AlertStore;
let settingsStore: Settings;
let silenceFormStore: SilenceFormStore;
let group: APIAlertGroupT;
let grid: APIGridT;
let originalInnerWidth: number;

const MockGroup = () => {
  const group = MockAlertGroup(
    [
      { name: "alertname", value: "Fake Alert" },
      { name: "groupName", value: "groupName" },
    ],
    [],
    [],
    [],
    {},
  );
  return group;
};

beforeAll(() => {
  originalInnerWidth = global.innerWidth;
});

beforeEach(() => {
  global.innerWidth = originalInnerWidth;
  alertStore = new AlertStore([]);
  settingsStore = new Settings(null);
  silenceFormStore = new SilenceFormStore();
  group = MockGroup();

  alertStore.data.setReceivers(["by-cluster-service", "by-name"]);
  alertStore.settings.setValues({
    ...alertStore.settings.values,
    ...{ historyEnabled: false },
  });

  grid = {
    labelName: "foo",
    labelValue: "bar",
    alertGroups: [],
    totalGroups: 0,
    stateCount: {
      active: 0,
      suppressed: 0,
      unprocessed: 0,
    },
  };
});

afterEach(() => {
  global.innerWidth = originalInnerWidth;
});

const MockAlerts = (alertCount: number, totalAlerts: number) => {
  for (let i = 1; i <= alertCount; i++) {
    const alert = MockAlert(
      [],
      [{ name: "instance", value: `instance${i}` }],
      "active",
    );
    const startsAt = new Date();
    alert.startsAt = startsAt.toISOString();
    alert.alertmanager[0].startsAt = startsAt.toISOString();
    group.alerts.push(alert);
  }
  group.totalAlerts = totalAlerts;
};

const renderAlertGroup = (afterUpdate: () => void, theme?: ThemeCtx) => {
  return render(
    <ThemeContext value={theme || MockThemeContext}>
      <AlertGroup
        afterUpdate={afterUpdate}
        grid={grid}
        group={group}
        settingsStore={settingsStore}
        alertStore={alertStore}
        silenceFormStore={silenceFormStore}
        gridLabelValue=""
        groupWidth={420}
      />
    </ThemeContext>,
  );
};

const ValidateCollapse = (
  innerWidth: number,
  defaultCollapseState: CollapseStateT,
  shouldBeCollapsed: boolean,
) => {
  global.innerWidth = innerWidth;

  settingsStore.alertGroupConfig.setDefaultCollapseState(defaultCollapseState);

  MockAlerts(3, 3);
  const { container } = renderAlertGroup(jest.fn());
  const alerts = container.querySelectorAll(
    ".components-grid-alertgrid-alertgroup-alert",
  );
  expect(alerts).toHaveLength(shouldBeCollapsed ? 0 : 3);
};

describe("<AlertGroup />", () => {
  it("doesn't crash on unmount", () => {
    MockAlerts(5, 5);
    const { unmount } = renderAlertGroup(jest.fn());
    unmount();
  });

  it("uses 'animate' class when settingsStore.themeConfig.config.animations is true", () => {
    MockAlerts(5, 5);
    const { container } = renderAlertGroup(jest.fn(), MockThemeContext);
    const groupEl = container.querySelector(
      "div.components-grid-alertgrid-alertgroup",
    );
    expect(groupEl?.classList.contains("animate")).toBe(true);
  });

  it("doesn't use 'animate' class when settingsStore.themeConfig.config.animations is false", () => {
    MockAlerts(5, 5);
    const { container } = renderAlertGroup(
      jest.fn(),
      MockThemeContextWithoutAnimations,
    );
    const groupEl = container.querySelector(
      "div.components-grid-alertgrid-alertgroup",
    );
    expect(groupEl?.classList.contains("animate")).toBe(false);
  });

  it("renders Alertmanager cluster labels in footer if shared", () => {
    alertStore.data.setUpstreams({
      counters: { total: 2, healthy: 2, failed: 0 },
      clusters: { default: ["default"], second: ["second"] },
      instances: [
        {
          name: "default",
          cluster: "default",
          clusterMembers: ["default"],
          uri: "http://localhost",
          publicURI: "http://localhost",
          error: "",
          version: "0.24.0",
          readonly: false,
          corsCredentials: "include",
          headers: {},
        },
        {
          name: "second",
          cluster: "second",
          clusterMembers: ["second"],
          uri: "http://localhost",
          publicURI: "http://localhost",
          error: "",
          version: "0.24.0",
          readonly: false,
          corsCredentials: "include",
          headers: {},
        },
      ],
    });
    MockAlerts(2, 2);
    group.shared.clusters = ["default", "second"];
    const { container } = renderAlertGroup(jest.fn());
    expect(container.innerHTML).toMatch(/@cluster/);
  });

  it("only renders one @cluster label per cluster in the footer", () => {
    alertStore.data.setUpstreams({
      counters: { total: 2, healthy: 2, failed: 0 },
      clusters: { default: ["default"], HA: ["ha1", "ha2"] },
      instances: [
        {
          name: "default",
          cluster: "default",
          clusterMembers: ["default"],
          uri: "http://localhost",
          publicURI: "http://localhost",
          error: "",
          version: "0.24.0",
          readonly: false,
          corsCredentials: "include",
          headers: {},
        },
        {
          name: "ha1",
          cluster: "HA",
          clusterMembers: ["ha1", "ha2"],
          uri: "http://localhost",
          publicURI: "http://localhost",
          error: "",
          version: "0.24.0",
          readonly: false,
          corsCredentials: "include",
          headers: {},
        },
        {
          name: "ha2",
          cluster: "HA",
          clusterMembers: ["ha1", "ha2"],
          uri: "http://localhost",
          publicURI: "http://localhost",
          error: "",
          version: "0.24.0",
          readonly: false,
          corsCredentials: "include",
          headers: {},
        },
      ],
    });
    MockAlerts(2, 2);
    for (let i = 0; i < group.alerts.length; i++) {
      group.alerts[i].alertmanager.push({
        fingerprint: "123",
        name: "ha1",
        cluster: "HA",
        state: "active",
        startsAt: "2018-08-14T17:36:40.017867056Z",
        source: "http://localhost/graph",
        silencedBy: [],
        inhibitedBy: [],
      });
      group.alerts[i].alertmanager.push({
        fingerprint: "123",
        name: "ha2",
        cluster: "HA",
        state: "active",
        startsAt: "2018-08-14T17:36:40.017867056Z",
        source: "http://localhost/graph",
        silencedBy: [],
        inhibitedBy: [],
      });
    }
    group.shared.clusters = ["default", "HA"];
    const { container } = renderAlertGroup(jest.fn());
    expect(container.textContent).toMatch(/@cluster:.*default/);
    expect(container.textContent).toMatch(/@cluster:.*HA/);
    expect(container.textContent).toMatch(/@receiver:.*by-name/);
  });

  it("doesn't render @cluster labels with empty alertmanager array", () => {
    MockAlerts(2, 2);
    for (let i = 0; i < group.alerts.length; i++) {
      group.alerts[i].alertmanager = [];
    }
    const { container } = renderAlertGroup(jest.fn());
    expect(container.textContent).not.toMatch(/@cluster/);
    expect(container.textContent).toMatch(/@receiver:.*by-name/);
  });

  it("doesn't render @cluster labels in footer when they are unique", () => {
    alertStore.data.setUpstreams({
      counters: { total: 2, healthy: 2, failed: 0 },
      clusters: { default: ["default"], second: ["second"] },
      instances: [
        {
          name: "default",
          cluster: "default",
          clusterMembers: ["default"],
          uri: "http://localhost",
          publicURI: "http://localhost",
          error: "",
          version: "0.24.0",
          readonly: false,
          corsCredentials: "include",
          headers: {},
        },
        {
          name: "second",
          cluster: "second",
          clusterMembers: ["second"],
          uri: "http://localhost",
          publicURI: "http://localhost",
          error: "",
          version: "0.24.0",
          readonly: false,
          corsCredentials: "include",
          headers: {},
        },
      ],
    });
    MockAlerts(5, 5);
    for (let i = 0; i < group.alerts.length; i++) {
      group.alerts[i].alertmanager[0].name = `fakeAlertmanager${i}`;
    }
    group.alertmanagerCount = {
      fakeAlertmanager0: 1,
      fakeAlertmanager1: 1,
      fakeAlertmanager2: 1,
      fakeAlertmanager3: 1,
      fakeAlertmanager4: 1,
    };
    const { container } = renderAlertGroup(jest.fn());

    const alerts = container.querySelector("ul.list-group");
    expect(alerts?.innerHTML).toMatch(/@cluster/);

    const footer = container.querySelector(".card-footer");
    expect(footer?.innerHTML).not.toMatch(/@cluster/);
  });

  it("only renders titlebar when collapsed", () => {
    MockAlerts(5, 10);
    const { container } = renderAlertGroup(jest.fn());
    const badges = container.querySelectorAll("span.badge.cursor-pointer");
    fireEvent.click(badges[1]);
    expect(
      container.querySelectorAll(".components-grid-alertgrid-alertgroup-alert"),
    ).toHaveLength(0);
    expect(container.querySelectorAll("ul.list-group")).toHaveLength(0);
  });

  it("renders reduced details when idle", () => {
    MockAlerts(5, 10);
    alertStore.ui.setIsIdle(true);
    const { container } = renderAlertGroup(jest.fn(), MockThemeContext);
    expect(
      container.querySelectorAll(".components-grid-alertgrid-alertgroup-alert"),
    ).toHaveLength(1);
  });

  it("is collapsed by default on desktop when defaultCollapseState=collapsed", () => {
    // set window.innerWidth to 2k to mock a desktop window
    ValidateCollapse(2048, "collapsed", true);
  });

  it("is collapsed by default on mobile when defaultCollapseState=collapsed", () => {
    // set window.innerWidth to <768 to mock a mobile window
    ValidateCollapse(767, "collapsed", true);
  });

  it("is expanded by default on desktop when defaultCollapseState=collapsedOnMobile", () => {
    // set window.innerWidth to 2k to mock a desktop window
    ValidateCollapse(2048, "collapsedOnMobile", false);
  });

  it("is collapsed by default on mobile when defaultCollapseState=collapsedOnMobile", () => {
    // set window.innerWidth to <768 to mock a mobile window
    ValidateCollapse(767, "collapsedOnMobile", true);
  });

  it("is expanded by default on desktop when defaultCollapseState=expanded", () => {
    // set window.innerWidth to 2k to mock a desktop window
    ValidateCollapse(2048, "expanded", false);
  });

  it("is expanded by default on mobile when defaultCollapseState=expanded", () => {
    // set window.innerWidth to <768 to mock a mobile window
    ValidateCollapse(767, "expanded", false);
  });

  it("renders @receiver label when alertStore.data.receivers.length > 1", () => {
    alertStore.data.setReceivers(["foo", "bar"]);
    MockAlerts(5, 10);
    const { container } = renderAlertGroup(jest.fn());
    expect(container.innerHTML).toMatch(/@receiver:/);
  });

  it("doesn't render @receiver label when alertStore.data.receivers.length == 0", () => {
    alertStore.data.setReceivers([]);
    MockAlerts(5, 10);
    const { container } = renderAlertGroup(jest.fn());
    expect(container.innerHTML).not.toMatch(/@receiver:/);
  });
});

const ValidateLoadButtonPresent = (
  alertCount: number,
  totalAlerts: number,
  isPresent: boolean,
) => {
  MockAlerts(alertCount, totalAlerts);
  const { container } = renderAlertGroup(jest.fn());
  const buttons = container.querySelectorAll("button");
  expect(buttons).toHaveLength(isPresent ? 2 : 0);
};

const ValidateLoadButtonAction = (
  alertCount: number,
  totalAlerts: number,
  buttonIndex: number,
  iconMatch: RegExp,
  loadedAlerts: number,
) => {
  MockAlerts(alertCount, totalAlerts);
  const { container } = renderAlertGroup(jest.fn());
  const buttons = container.querySelectorAll("button");
  expect(buttons[buttonIndex].innerHTML).toMatch(iconMatch);
  fireEvent.click(buttons[buttonIndex]);
  expect(alertStore.ui.groupAlertLimits[group.id]).toBe(loadedAlerts);
};

describe("<AlertGroup /> renderConfig", () => {
  it("settingsStore.alertGroupConfig.config.defaultRenderCount should be 5 by default", () => {
    expect(settingsStore.alertGroupConfig.config.defaultRenderCount).toBe(5);
  });

  it("load buttons are not rendered for 1 alert", () => {
    ValidateLoadButtonPresent(1, 1, false);
  });

  it("load buttons are not rendered for 5 alerts", () => {
    ValidateLoadButtonPresent(5, 5, false);
  });

  it("load buttons are rendered for 6 alert", () => {
    ValidateLoadButtonPresent(5, 6, true);
  });

  it("clicking - icon hides 1 alert if there's 6 in total", () => {
    ValidateLoadButtonAction(6, 6, 0, /fa-minus/, 5);
  });

  it("clicking - icon hides 1 alert if there's 6 in total and we're showing 3", () => {
    ValidateLoadButtonAction(3, 6, 0, /fa-minus/, 2);
  });

  it("clicking - icon hides 2 alerts if there's 7 in total and we're showing 7", () => {
    ValidateLoadButtonAction(7, 7, 0, /fa-minus/, 5);
  });

  it("clicking - icon hides 5 alerts if there's 10 in total and we're showing 10", () => {
    ValidateLoadButtonAction(10, 10, 0, /fa-minus/, 5);
  });

  it("clicking - icon hides 5 alerts if there's 18 in total and we're showing 17", () => {
    ValidateLoadButtonAction(17, 18, 0, /fa-minus/, 12);
  });

  it("clicking + icon loads 1 more alert if there's 6 in total", () => {
    ValidateLoadButtonAction(5, 6, 1, /fa-plus/, 6);
  });

  it("clicking + icon loads 4 more alert if there's 9 in total", () => {
    ValidateLoadButtonAction(5, 9, 1, /fa-plus/, 9);
  });

  it("clicking + icon loads 5 more alert if there's 14 in total", () => {
    ValidateLoadButtonAction(5, 14, 1, /fa-plus/, 10);
  });

  it("clicking + icon loads 5 more alert if there's 25 in total and we're showing 16", () => {
    ValidateLoadButtonAction(16, 25, 1, /fa-plus/, 21);
  });

  it("uses 'z-index: 100' style after setIsMenuOpen() is called on any Alert", async () => {
    fetchMock.mockReset();
    fetchMock.route("*", { body: "" });

    const promise = Promise.resolve();
    MockAlerts(5, 5);
    const { container } = renderAlertGroup(jest.fn());

    const alertMenuToggle = container.querySelector(
      ".components-grid-alertgrid-alertgroup-alert span.bg-secondary.cursor-pointer",
    );
    fireEvent.click(alertMenuToggle!);
    await act(() => promise);
    const groupEl = container.querySelector(
      ".components-grid-alertgrid-alertgroup",
    ) as HTMLElement;
    expect(groupEl?.style.zIndex).toBe("100");
  });

  it("uses 'z-index: 100' style after setIsMenuOpen() is called on AlertGroup header menu", async () => {
    const promise = Promise.resolve();
    MockAlerts(5, 5);
    const { container } = renderAlertGroup(jest.fn());

    const menuToggle = container.querySelector("span.cursor-pointer");
    fireEvent.click(menuToggle!);
    await act(() => promise);
    const groupEl = container.querySelector(
      ".components-grid-alertgrid-alertgroup",
    ) as HTMLElement;
    expect(groupEl?.style.zIndex).toBe("100");
  });
});

describe("<AlertGroup /> card theme", () => {
  it("renders bg-light background when colorTitleBar=false", () => {
    settingsStore.alertGroupConfig.setColorTitleBar(false);
    group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
    const { container } = renderAlertGroup(jest.fn());
    const card = container.querySelector(".card");
    expect(card?.classList.contains("bg-light")).toBe(true);
    expect(card?.classList.contains("bg-danger")).toBe(false);
    expect(card?.classList.contains("bg-success")).toBe(false);
    expect(card?.classList.contains("bg-secondary")).toBe(false);
  });

  it("renders themed titlebar when colorTitleBar=false", () => {
    settingsStore.alertGroupConfig.setColorTitleBar(false);
    group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
    const { container } = renderAlertGroup(jest.fn());
    expect(container.querySelector(".badge.bg-danger")).toBeInTheDocument();
  });

  it("renders bg-light border when colorTitleBar=true and there are multiple alert states", () => {
    settingsStore.alertGroupConfig.setColorTitleBar(false);
    group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
    const { container } = renderAlertGroup(jest.fn());
    const card = container.querySelector(".card");
    expect(card?.classList.contains("bg-light")).toBe(true);
    expect(card?.classList.contains("bg-danger")).toBe(false);
    expect(card?.classList.contains("bg-success")).toBe(false);
    expect(card?.classList.contains("bg-secondary")).toBe(false);
  });

  it("renders themed titlebar when colorTitleBar=true and there are multiple alert states", () => {
    settingsStore.alertGroupConfig.setColorTitleBar(true);
    group.stateCount = { active: 5, suppressed: 6, unprocessed: 7 };
    const { container } = renderAlertGroup(jest.fn());
    expect(container.querySelector(".badge.bg-danger")).toBeInTheDocument();
  });

  it("renders state based background when colorTitleBar=true and there's only one alert state", () => {
    settingsStore.alertGroupConfig.setColorTitleBar(true);
    group.stateCount = { active: 0, suppressed: 5, unprocessed: 0 };
    const { container } = renderAlertGroup(jest.fn());
    const card = container.querySelector(".card");
    expect(card?.classList.contains("bg-light")).toBe(false);
    expect(card?.classList.contains("bg-danger")).toBe(false);
    expect(card?.classList.contains("bg-success")).toBe(true);
    expect(card?.classList.contains("bg-secondary")).toBe(false);
  });

  it("renders unthemed titlebar when colorTitleBar=true and there's only one alert state", () => {
    settingsStore.alertGroupConfig.setColorTitleBar(true);
    group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
    const { container } = renderAlertGroup(jest.fn());
    const card = container.querySelector(".card");
    expect(card?.classList.contains("bg-danger")).toBe(true);
  });

  it("renders AlertHistory when enabled", async () => {
    fetchMock.mockReset();
    fetchMock.route(
      "/history.json",
      {
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(RainbowHistoryResponse),
      },
      {
        overwriteRoutes: true,
      },
    );

    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{ historyEnabled: true },
    });
    group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
    const { container } = renderAlertGroup(jest.fn());
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    expect(container.innerHTML).toMatch(/alert-history/);
    expect(fetchMock.callHistory.calls()).toHaveLength(1);
    expect(fetchMock.callHistory.calls()[0]?.url).toContain("/history.json");
  });

  it("doesn't render AlertHistory when disabled", () => {
    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{ historyEnabled: false },
    });
    group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
    const { container } = renderAlertGroup(jest.fn());
    expect(container.innerHTML).not.toMatch(/alert-history/);
  });
});
