import { act } from "react";

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

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

import { MockAlertGroup, MockAlert } from "__fixtures__/Alerts";
import type { APIAlertT, APIAlertGroupT } from "Models/APITypes";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { AlertAck } from ".";

let alertStore: AlertStore;
let silenceFormStore: SilenceFormStore;
let alerts: APIAlertT[];
let group: APIAlertGroupT;

beforeEach(() => {
  jest.useFakeTimers();
  jest.setSystemTime(Date.UTC(2000, 1, 1, 0, 0, 0));

  alertStore = new AlertStore([]);
  silenceFormStore = new SilenceFormStore();

  alertStore.settings.setValues({
    ...alertStore.settings.values,
    ...{
      alertAcknowledgement: {
        enabled: true,
        durationSeconds: 123,
        author: "default author",
        comment: "COMMENT",
      },
    },
  });

  alertStore.data.setUpstreams({
    counters: { total: 1, healthy: 1, failed: 0 },
    clusters: { default: ["default"] },
    instances: [
      {
        name: "default",
        uri: "http://localhost",
        publicURI: "http://example.com",
        readonly: false,
        headers: { foo: "bar" },
        corsCredentials: "include",
        error: "",
        version: "0.24.0",
        cluster: "default",
        clusterMembers: ["default"],
      },
    ],
  });

  alerts = [
    MockAlert([], [{ name: "foo", value: "bar" }], "active"),
    MockAlert([], [{ name: "foo", value: "baz" }], "active"),
    MockAlert([], [{ name: "foo", value: "ignore" }], "suppressed"),
  ];
  group = MockAlertGroup(
    [{ name: "alertname", value: "Fake Alert" }],
    alerts,
    [],
    [],
    {},
  );
  group.allLabels.active = {
    alertname: ["Fake Alert"],
    foo: ["bar", "baz"],
  };

  fetchMock.mockReset();
  fetchMock.route(
    "*",
    {
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ silenceID: "123" }),
    },
    {
      overwriteRoutes: true,
    },
  );
});

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

const renderAlertAck = () => {
  return render(
    <AlertAck
      alertStore={alertStore}
      silenceFormStore={silenceFormStore}
      group={group}
    />,
  );
};

const renderAndClick = async () => {
  renderAlertAck();
  const badge = screen.getByRole("img", { hidden: true }).parentElement;
  if (badge) fireEvent.click(badge);
  await act(async () => {
    await fetchMock.callHistory.flush(true);
  });
};

describe("<AlertAck />", () => {
  it("is null when acks are disabled", () => {
    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{
        alertAcknowledgement: {
          enabled: false,
          durationSeconds: 123,
          author: "default author",
          comment: "COMMENT",
        },
      },
    });
    const { container } = renderAlertAck();
    expect(container).toBeEmptyDOMElement();
  });

  it("uses faCheck icon when idle", () => {
    renderAlertAck();
    expect(screen.getByRole("img", { hidden: true })).toHaveClass("fa-check");
  });

  it("uses faExclamationCircle after failed fetch", async () => {
    fetchMock.mockReset();
    fetchMock.route("*", {
      status: 500,
      body: "error message",
    });
    renderAlertAck();
    const badge = screen.getByRole("img", { hidden: true }).parentElement;
    if (badge) fireEvent.click(badge);
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    await waitFor(() => {
      expect(screen.getByRole("img", { hidden: true })).toHaveClass(
        "fa-circle-exclamation",
      );
    });
  });

  it("resets faExclamationCircle after 20s", async () => {
    fetchMock.mockReset();
    fetchMock.route("*", {
      status: 500,
      body: "error message",
    });
    renderAlertAck();
    const badge = screen.getByRole("img", { hidden: true }).parentElement;
    if (badge) fireEvent.click(badge);
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    await waitFor(() => {
      expect(screen.getByRole("img", { hidden: true })).toHaveClass(
        "fa-circle-exclamation",
      );
    });

    act(() => {
      jest.advanceTimersByTime(21 * 1000);
    });
    await waitFor(() => {
      expect(screen.getByRole("img", { hidden: true })).not.toHaveClass(
        "fa-circle-exclamation",
      );
      expect(screen.getByRole("img", { hidden: true })).toHaveClass("fa-check");
    });
  });

  it("uses faCheckCircle after successful fetch", async () => {
    renderAlertAck();
    const badge = screen.getByRole("img", { hidden: true }).parentElement;
    if (badge) fireEvent.click(badge);
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    await waitFor(() => {
      expect(screen.getByRole("img", { hidden: true })).toHaveClass(
        "fa-circle-check",
      );
    });
  });

  it("sends a POST request on click", async () => {
    await renderAndClick();
    expect(fetchMock.callHistory.calls()).toHaveLength(1);
    expect(fetchMock.callHistory.lastCall()?.options).toMatchObject({
      method: "post",
      headers: { "content-type": "application/json" },
    });
  });

  it("sends a POST request to every cluster", async () => {
    alertStore.data.setUpstreams({
      counters: { total: 4, healthy: 4, failed: 0 },
      clusters: { c1: ["m1", "m2"], c2: ["m3", "m4"] },
      instances: ["m1", "m2", "m3", "m4"].map((a) => ({
        name: a,
        uri: `http://${a}.example.com`,
        publicURI: `http://${a}.example.com`,
        readonly: false,
        headers: { "X-Cluster": a === "m1" || a === "2" ? "c1" : "c2" },
        corsCredentials: a === "m1" || a === "2" ? "same-origin" : "include",
        error: "",
        version: "0.24.0",
        cluster: a === "m1" || a === "2" ? "c1" : "c2",
        clusterMembers: a === "m1" || a === "2" ? ["m1", "m2"] : ["m3", "m4"],
      })),
    });
    group.alertmanagerCount = {
      m1: 1,
      m2: 1,
      m3: 1,
      m4: 1,
    };

    await renderAndClick();
    expect(fetchMock.callHistory.calls()).toHaveLength(2);
    expect(fetchMock.callHistory.calls()[0]?.url).toBe(
      "http://m1.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[0]?.options).toMatchObject({
      method: "post",
      credentials: "same-origin",
      headers: { "x-cluster": "c1" },
    });
    expect(fetchMock.callHistory.calls()[1]?.url).toBe(
      "http://m3.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[1]?.options).toMatchObject({
      method: "post",
      credentials: "include",
      headers: { "x-cluster": "c2" },
    });
  });

  it("skips readonly alertmanagers", async () => {
    alertStore.data.setUpstreams({
      counters: { total: 4, healthy: 4, failed: 0 },
      clusters: { c1: ["m1", "m2"], c2: ["m3", "m4"] },
      instances: ["m1", "m2", "m3", "m4"].map((a) => ({
        name: a,
        uri: `http://${a}.example.com`,
        publicURI: `http://${a}.example.com`,
        readonly: a === "m1" || a === "m3" ? true : false,
        headers: { "X-Cluster": a === "m1" || a === "2" ? "c1" : "c2" },
        corsCredentials: a === "m1" || a === "m2" ? "same-origin" : "include",
        error: "",
        version: "0.24.0",
        cluster: a === "m1" || a === "2" ? "c1" : "c2",
        clusterMembers: a === "m1" || a === "2" ? ["m1", "m2"] : ["m3", "m4"],
      })),
    });
    group.alertmanagerCount = {
      m1: 1,
      m2: 1,
      m3: 1,
      m4: 1,
    };

    await renderAndClick();
    expect(fetchMock.callHistory.calls()).toHaveLength(2);
    expect(fetchMock.callHistory.calls()[0]?.url).toBe(
      "http://m2.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[1]?.url).toBe(
      "http://m4.example.com/api/v2/silences",
    );
  });

  it("doesn't send any request on click when already done", async () => {
    const { container } = renderAlertAck();
    const button = container.querySelector("span.badge");

    fireEvent.click(button!);
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    expect(fetchMock.callHistory.calls()).toHaveLength(1);

    fireEvent.click(button!);
    expect(fetchMock.callHistory.calls()).toHaveLength(1);
  });

  it("sends correct payload", async () => {
    fetchMock.route(
      "*",
      {
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ silenceID: "123456789" }),
      },
      {
        overwriteRoutes: true,
      },
    );

    silenceFormStore.data.setAuthor("karma/ui");
    await renderAndClick();
    expect(
      JSON.parse(fetchMock.callHistory.lastCall()!.options.body as string),
    ).toEqual({
      comment: "COMMENT",
      createdBy: "karma/ui",
      endsAt: "2000-02-01T00:02:03.000Z",
      matchers: [
        {
          isRegex: false,
          name: "alertname",
          value: "Fake Alert",
          isEqual: true,
        },
        { isRegex: true, name: "foo", value: "(bar|baz)", isEqual: true },
      ],
      startsAt: "2000-02-01T00:00:00.000Z",
    });
  });

  it("uses settings when generating payload", async () => {
    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{
        alertAcknowledgement: {
          enabled: true,
          durationSeconds: 237,
          author: "me",
          comment: "comment",
        },
      },
    });
    await renderAndClick();
    expect(
      JSON.parse(fetchMock.callHistory.lastCall()!.options.body as string),
    ).toEqual({
      comment: "comment",
      createdBy: "me",
      endsAt: "2000-02-01T00:03:57.000Z",
      matchers: [
        {
          isRegex: false,
          name: "alertname",
          value: "Fake Alert",
          isEqual: true,
        },
        { isRegex: true, name: "foo", value: "(bar|baz)", isEqual: true },
      ],
      startsAt: "2000-02-01T00:00:00.000Z",
    });
  });

  it("injects UTC timestamp when configured", async () => {
    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{
        alertAcknowledgement: {
          enabled: true,
          durationSeconds: 237,
          author: "me",
          comment: "ACK! This alert was acknowledged using karma on %NOW%",
        },
      },
    });
    await renderAndClick();
    expect(
      JSON.parse(fetchMock.callHistory.lastCall()!.options.body as string),
    ).toEqual({
      comment:
        "ACK! This alert was acknowledged using karma on Tue, 01 Feb 2000 00:00:00 GMT",
      createdBy: "me",
      endsAt: "2000-02-01T00:03:57.000Z",
      matchers: [
        {
          isRegex: false,
          name: "alertname",
          value: "Fake Alert",
          isEqual: true,
        },
        { isRegex: true, name: "foo", value: "(bar|baz)", isEqual: true },
      ],
      startsAt: "2000-02-01T00:00:00.000Z",
    });
  });

  it("injects local timezone timestamp when configured", async () => {
    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{
        alertAcknowledgement: {
          enabled: true,
          durationSeconds: 237,
          author: "me",
          comment: "ACK! This alert was acknowledged using karma on %NOWLOC%",
        },
      },
    });
    await renderAndClick();
    const comment = JSON.parse(
      fetchMock.callHistory.lastCall()!.options.body as string,
    ).comment;
    expect(comment).not.toEqual(
      "ACK! This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT",
    );
    expect(comment).toMatch(
      /ACK! This alert was acknowledged using karma on (Mon Jan 31 2000 19|Tue Feb 01 2000 00):00:00 GMT([+-]+)[0-9]+ \(.*\)/,
    );
  });

  it("uses author from authentication info when auth is enabled", async () => {
    alertStore.info.setAuthentication(true, "auth@example.com");
    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{
        alertAcknowledgement: {
          enabled: true,
          durationSeconds: 222,
          author: "me",
          comment: "FOO: bar",
        },
      },
    });
    await renderAndClick();
    expect(
      JSON.parse(fetchMock.callHistory.lastCall()!.options.body as string),
    ).toEqual({
      comment: "FOO: bar",
      createdBy: "auth@example.com",
      endsAt: "2000-02-01T00:03:42.000Z",
      matchers: [
        {
          isRegex: false,
          name: "alertname",
          value: "Fake Alert",
          isEqual: true,
        },
        { isRegex: true, name: "foo", value: "(bar|baz)", isEqual: true },
      ],
      startsAt: "2000-02-01T00:00:00.000Z",
    });
  });

  it("uses author from silenceFormStore if authentication is disabled", async () => {
    alertStore.info.setAuthentication(false, "wrong");
    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{
        alertAcknowledgement: {
          enabled: true,
          durationSeconds: 222,
          author: "me",
          comment: "FOO: bar",
        },
      },
    });
    silenceFormStore.data.setAuthor("bob@example.com");
    await renderAndClick();
    expect(
      JSON.parse(fetchMock.callHistory.lastCall()!.options.body as string),
    ).toEqual({
      comment: "FOO: bar",
      createdBy: "bob@example.com",
      endsAt: "2000-02-01T00:03:42.000Z",
      matchers: [
        {
          isRegex: false,
          name: "alertname",
          value: "Fake Alert",
          isEqual: true,
        },
        { isRegex: true, name: "foo", value: "(bar|baz)", isEqual: true },
      ],
      startsAt: "2000-02-01T00:00:00.000Z",
    });
  });

  it("uses default author as fallback", async () => {
    alertStore.settings.setValues({
      ...alertStore.settings.values,
      ...{
        alertAcknowledgement: {
          enabled: true,
          durationSeconds: 222,
          author: "me",
          comment: "FOO: bar",
        },
      },
    });
    silenceFormStore.data.setAuthor("");
    await renderAndClick();
    expect(
      JSON.parse(fetchMock.callHistory.lastCall()!.options.body as string),
    ).toEqual({
      comment: "FOO: bar",
      createdBy: "me",
      endsAt: "2000-02-01T00:03:42.000Z",
      matchers: [
        {
          isRegex: false,
          name: "alertname",
          value: "Fake Alert",
          isEqual: true,
        },
        { isRegex: true, name: "foo", value: "(bar|baz)", isEqual: true },
      ],
      startsAt: "2000-02-01T00:00:00.000Z",
    });
  });

  it("sends POST request to /api/v2/silences", async () => {
    await renderAndClick();
    const uri = fetchMock.callHistory.calls()[0]?.url;
    expect(uri).toBe("http://localhost/api/v2/silences");
  });

  it("will retry on another cluster member after 500 response", async () => {
    fetchMock.mockReset();
    fetchMock.route("http://m1.example.com/api/v2/silences", {
      status: 500,
      body: "error message",
    });
    fetchMock.route("http://m2.example.com/api/v2/silences", {
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ silenceID: "123" }),
    });
    fetchMock.route("http://m3.example.com/api/v2/silences", {
      status: 500,
      body: "error message",
    });
    fetchMock.route("http://m4.example.com/api/v2/silences", {
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ silenceID: "456" }),
    });
    alertStore.data.setUpstreams({
      counters: { total: 1, healthy: 1, failed: 0 },
      clusters: { c1: ["m1", "m2"], c2: ["m3", "m4"] },
      instances: ["m1", "m2", "m3", "m4"].map((a) => ({
        name: a,
        uri: `http://${a}.example.com`,
        publicURI: `http://${a}.example.com`,
        readonly: false,
        headers: { "X-Cluster": a === "m1" || a === "2" ? "c1" : "c2" },
        corsCredentials: a === "m1" || a === "m2" ? "same-origin" : "include",
        error: "",
        version: "0.24.0",
        cluster: a === "m1" || a === "2" ? "c1" : "c2",
        clusterMembers: a === "m1" || a === "2" ? ["m1", "m2"] : ["m3", "m4"],
      })),
    });
    group.alertmanagerCount = {
      m1: 1,
      m2: 1,
      m3: 1,
      m4: 1,
    };

    const { container } = renderAlertAck();
    const button = container.querySelector("span.badge");
    fireEvent.click(button!);
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    expect(fetchMock.callHistory.calls()).toHaveLength(4);
    expect(fetchMock.callHistory.calls()[0]?.url).toBe(
      "http://m1.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[1]?.url).toBe(
      "http://m2.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[2]?.url).toBe(
      "http://m3.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[3]?.url).toBe(
      "http://m4.example.com/api/v2/silences",
    );
  });

  it("will retry on another cluster member after fetch failure", async () => {
    fetchMock.mockReset();
    fetchMock.route("http://m1.example.com/api/v2/silences", {
      throws: new TypeError("failed to fetch"),
    });
    fetchMock.route("http://m2.example.com/api/v2/silences", {
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ silenceID: "123" }),
    });
    fetchMock.route("http://m3.example.com/api/v2/silences", {
      throws: new TypeError("failed to fetch"),
    });
    fetchMock.route("http://m4.example.com/api/v2/silences", {
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ silenceID: "123" }),
    });
    alertStore.data.setUpstreams({
      counters: { total: 1, healthy: 1, failed: 0 },
      clusters: { c1: ["m1", "m2"], c2: ["m3", "m4"] },
      instances: ["m1", "m2", "m3", "m4"].map((a) => ({
        name: a,
        uri: `http://${a}.example.com`,
        publicURI: `http://${a}.example.com`,
        readonly: false,
        headers: { "X-Cluster": a === "m1" || a === "2" ? "c1" : "c2" },
        corsCredentials: a === "m1" || a === "m2" ? "same-origin" : "include",
        error: "",
        version: "0.24.0",
        cluster: a === "m1" || a === "2" ? "c1" : "c2",
        clusterMembers: a === "m1" || a === "2" ? ["m1", "m2"] : ["m3", "m4"],
      })),
    });
    group.alertmanagerCount = {
      m1: 1,
      m2: 1,
      m3: 1,
      m4: 1,
    };

    const { container } = renderAlertAck();
    const button = container.querySelector("span.badge");
    fireEvent.click(button!);
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    expect(fetchMock.callHistory.calls()).toHaveLength(4);
    expect(fetchMock.callHistory.calls()[0]?.url).toBe(
      "http://m1.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[1]?.url).toBe(
      "http://m2.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[2]?.url).toBe(
      "http://m3.example.com/api/v2/silences",
    );
    expect(fetchMock.callHistory.calls()[3]?.url).toBe(
      "http://m4.example.com/api/v2/silences",
    );
  });

  it("will log an error if Alertmanager instance is missing from instances and try the next one", async () => {
    const consoleSpy = jest
      .spyOn(console, "error")
      .mockImplementation(() => {});
    alertStore.data.setUpstreams({
      counters: { total: 1, healthy: 1, failed: 0 },
      clusters: { default: ["default", "fallback"] },
      instances: [
        {
          name: "default",
          uri: "http://am1.example.com",
          publicURI: "http://am1.example.com",
          readonly: false,
          headers: {},
          corsCredentials: "include",
          error: "",
          version: "0.24.0",
          cluster: "default",
          clusterMembers: ["default", "fallback"],
        },
      ],
    });

    const { container } = renderAlertAck();
    const button = container.querySelector("span.badge");
    fireEvent.click(button!);
    await act(async () => {
      await fetchMock.callHistory.flush(true);
    });
    expect(fetchMock.callHistory.calls()).toHaveLength(1);
    expect(fetchMock.callHistory.calls()[0]?.url).toBe(
      "http://am1.example.com/api/v2/silences",
    );
    expect(consoleSpy).toHaveBeenCalledTimes(1);
  });
});
