import React from "react";
import { shallow, mount } from "enzyme";
import { ContentTiles } from "content-src/components/ContentTiles";
import { ActionChecklist } from "content-src/components/ActionChecklist";
import { MobileDownloads } from "content-src/components/MobileDownloads";
import { AboutWelcomeUtils } from "content-src/lib/aboutwelcome-utils.mjs";
import { GlobalOverrider } from "asrouter/tests/unit/utils";

describe("ContentTiles component", () => {
  let sandbox;
  let wrapper;
  let handleAction;
  let setActiveMultiSelect;
  let setActiveSingleSelectSelection;
  let globals;

  const CHECKLIST_TILE = {
    type: "action_checklist",
    header: {
      title: "Checklist Header",
      subtitle: "Checklist Subtitle",
      style: {
        border: "1px solid #ccc",
      },
    },
    data: [
      {
        id: "action-checklist-test",
        targeting: "false",
        label: {
          raw: "Test label",
        },
        action: {
          data: {
            pref: {
              name: "messaging-system-action.test1",
              value: "false",
            },
          },
          type: "SET_PREF",
        },
      },
    ],
  };

  const MOBILE_TILE = {
    type: "mobile_downloads",
    header: {
      title: "Mobile Header",
      style: {
        backgroundColor: "#e0e0e0",
        border: "1px solid #999",
      },
    },
    data: {
      email: {
        link_text: "Email yourself a link",
      },
    },
  };

  const TITLE_TILE = {
    type: "mobile_downloads",
    title: "Tile Title",
    subtitle: "Tile Subtitle",
    data: {
      email: {
        link_text: "Email yourself a link",
      },
    },
  };

  const TEST_CONTENT = {
    tiles: [CHECKLIST_TILE, MOBILE_TILE],
  };

  beforeEach(() => {
    sandbox = sinon.createSandbox();
    handleAction = sandbox.stub();
    setActiveMultiSelect = sandbox.stub();
    setActiveSingleSelectSelection = sandbox.stub();
    globals = new GlobalOverrider();
    globals.set({
      AWSendToDeviceEmailsSupported: () => Promise.resolve(),
    });
    wrapper = shallow(
      <ContentTiles
        content={TEST_CONTENT}
        handleAction={handleAction}
        activeMultiSelect={null}
        setActiveMultiSelect={setActiveMultiSelect}
      />
    );
  });

  afterEach(() => {
    sandbox.restore();
    globals.restore();
  });

  it("should render the component when tiles are provided", () => {
    assert.ok(wrapper.exists());
  });

  it("should not render the component when no tiles are provided", () => {
    wrapper.setProps({ content: {} });
    assert.ok(wrapper.isEmptyRender());
  });

  it("should render the correct number of tiles", () => {
    assert.equal(wrapper.find(".content-tile").length, 2);
  });

  it("should toggle a tile and send telemetry when its header is clicked", () => {
    let telemetrySpy = sandbox.spy(AboutWelcomeUtils, "sendActionTelemetry");
    const [firstTile] = TEST_CONTENT.tiles;
    const tileId = `${firstTile.type}${firstTile.id ? "_" : ""}${
      firstTile.id ?? ""
    }_header`;
    const firstTileButton = wrapper.find(".tile-header").at(0);
    firstTileButton.simulate("click");
    assert.equal(
      wrapper.find(".tile-content").at(0).prop("id"),
      "tile-content-0"
    );
    assert.equal(wrapper.find(".tile-content").at(0).exists(), true);
    assert.calledOnce(telemetrySpy);
    assert.equal(telemetrySpy.firstCall.args[1], tileId);
  });

  it("should only expand one tile at a time", () => {
    const firstTileButton = wrapper.find(".tile-header").at(0);
    firstTileButton.simulate("click");
    const secondTileButton = wrapper.find(".tile-header").at(1);
    secondTileButton.simulate("click");

    assert.equal(wrapper.find(".tile-content").length, 1);
    assert.equal(
      wrapper.find(".tile-content").at(0).prop("id"),
      "tile-content-1"
    );
  });

  it("should toggle all tiles and send telemetry when the tiles header is clicked", () => {
    const TEST_CONTENT_HEADER = {
      tiles: [CHECKLIST_TILE, MOBILE_TILE],
      tiles_header: {
        title: "Toggle Tiles Header",
      },
    };

    wrapper = mount(
      <ContentTiles
        content={TEST_CONTENT_HEADER}
        handleAction={handleAction}
        activeMultiSelect={null}
        setActiveMultiSelect={setActiveMultiSelect}
      />
    );

    let telemetrySpy = sandbox.spy(AboutWelcomeUtils, "sendActionTelemetry");
    const tilesHeaderButton = wrapper.find(".content-tiles-header");

    assert.ok(tilesHeaderButton.exists(), "Tiles header button should exist");
    tilesHeaderButton.simulate("click");

    assert.equal(
      wrapper.find("#content-tiles-container").exists(),
      true,
      "Content tiles container should be visible after toggle"
    );
    assert.calledOnce(telemetrySpy);
    assert.equal(
      telemetrySpy.firstCall.args[1],
      "content_tiles_header",
      "Telemetry should be sent for tiles header toggle"
    );

    tilesHeaderButton.simulate("click");
    assert.equal(
      wrapper.find("#content-tiles-container").exists(),
      false,
      "Content tiles container should not be visible after second toggle"
    );
  });

  it("should apply configured styles to the header buttons", () => {
    const mountedWrapper = mount(
      <ContentTiles
        content={TEST_CONTENT}
        handleAction={() => {}}
        activeMultiSelect={null}
        setActiveMultiSelect={setActiveMultiSelect}
      />
    );

    const firstTileHeader = mountedWrapper
      .find(".tile-header")
      .at(0)
      .getDOMNode();
    const secondTileHeader = mountedWrapper
      .find(".tile-header")
      .at(1)
      .getDOMNode();

    assert.equal(
      firstTileHeader.style.cssText,
      "border: 1px solid rgb(204, 204, 204);",
      "First tile header styles should match configured values"
    );
    assert.equal(
      secondTileHeader.style.cssText,
      "background-color: rgb(224, 224, 224); border: 1px solid rgb(153, 153, 153);",
      "Second tile header styles should match configured values"
    );

    mountedWrapper.unmount();
  });

  it("should render ActionChecklist for 'action_checklist' tile type", () => {
    const firstTileButton = wrapper.find(".tile-header").at(0);
    assert.ok(firstTileButton.exists(), "Tile header button should exist");
    firstTileButton.simulate("click");

    const actionChecklist = wrapper.find(ActionChecklist);
    assert.ok(actionChecklist.exists());
    assert.deepEqual(actionChecklist.prop("content").tiles[0].data, [
      {
        id: "action-checklist-test",
        targeting: "false",
        label: {
          raw: "Test label",
        },
        action: {
          data: {
            pref: {
              name: "messaging-system-action.test1",
              value: "false",
            },
          },
          type: "SET_PREF",
        },
      },
    ]);
  });

  it("should render MobileDownloads for 'mobile_downloads' tile type", () => {
    const secondTileButton = wrapper.find(".tile-header").at(1);
    assert.ok(secondTileButton.exists(), "Tile header button should exist");
    secondTileButton.simulate("click");

    const mobileDownloads = wrapper.find(MobileDownloads);
    assert.ok(mobileDownloads.exists());
    assert.deepEqual(mobileDownloads.prop("data"), {
      email: {
        link_text: "Email yourself a link",
      },
    });
    assert.equal(mobileDownloads.prop("handleAction"), handleAction);
  });

  it("should handle a single tile object", () => {
    wrapper.setProps({
      content: {
        tiles: {
          type: "action_checklist",
          data: [
            {
              id: "action-checklist-single",
              targeting: "false",
              label: {
                raw: "Single tile label",
              },
              action: {
                data: {
                  pref: {
                    name: "messaging-system-action.single",
                    value: "false",
                  },
                },
                type: "SET_PREF",
              },
            },
          ],
        },
      },
    });

    const actionChecklist = wrapper.find(ActionChecklist);
    assert.ok(actionChecklist.exists());
    assert.deepEqual(actionChecklist.prop("content").tiles.data, [
      {
        id: "action-checklist-single",
        targeting: "false",
        label: {
          raw: "Single tile label",
        },
        action: {
          data: {
            pref: {
              name: "messaging-system-action.single",
              value: "false",
            },
          },
          type: "SET_PREF",
        },
      },
    ]);
  });

  it("should prefill activeMultiSelect for a MultiSelect tile based on default values", () => {
    const MULTISELECT_TILE = {
      type: "multiselect",
      header: {
        title: "Multiselect Header",
        style: {
          border: "1px solid #ddd",
        },
      },
      data: [
        { id: "option1", defaultValue: true },
        { id: "option2", defaultValue: false },
        { id: "option3", defaultValue: true },
      ],
    };

    const contentWithMultiselect = { tiles: MULTISELECT_TILE };

    wrapper = mount(
      <ContentTiles
        content={contentWithMultiselect}
        activeMultiSelect={null}
        setActiveMultiSelect={setActiveMultiSelect}
        handleAction={handleAction}
      />
    );
    wrapper.update();

    sinon.assert.calledOnce(setActiveMultiSelect);
    sinon.assert.calledWithExactly(
      setActiveMultiSelect,
      ["option1", "option3"],
      "tile-0"
    );
  });

  it("should not prefill activeMultiSelect if it is already set", () => {
    const MULTISELECT_TILE = {
      type: "multiselect",
      header: {
        title: "Multiselect Header",
        style: {
          border: "1px solid #ddd",
        },
      },
      data: [
        { id: "option1", defaultValue: true },
        { id: "option2", defaultValue: false },
        { id: "option3", defaultValue: true },
      ],
    };

    const contentWithMultiselect = { tiles: [MULTISELECT_TILE] };

    wrapper = mount(
      <ContentTiles
        content={contentWithMultiselect}
        activeMultiSelect={["option2"]}
        setActiveMultiSelect={setActiveMultiSelect}
        handleAction={handleAction}
      />
    );
    wrapper.update();

    sinon.assert.notCalled(setActiveMultiSelect);
  });

  it("should render title and subtitle if present", () => {
    sandbox.stub(window, "AWSendToDeviceEmailsSupported").resolves(true);

    let TEST_TILE_CONTENT = {
      tiles: [TITLE_TILE],
    };

    const mountedWrapper = mount(
      <ContentTiles
        content={TEST_TILE_CONTENT}
        handleAction={() => {}}
        activeMultiSelect={null}
        setActiveMultiSelect={setActiveMultiSelect}
      />
    );

    const tileTitle = mountedWrapper.find(".tile-title");
    const tileSubtitle = mountedWrapper.find(".tile-subtitle");

    assert.ok(tileTitle.exists(), "Title should render");
    assert.ok(tileSubtitle.exists(), "Subtitle should render");

    assert.equal(
      tileTitle.text(),
      "Tile Title",
      "Tile title should have correct text"
    );
    assert.equal(
      tileSubtitle.text(),
      "Tile Subtitle",
      "Tile subtitle should have correct text"
    );

    mountedWrapper.unmount();
  });

  it("should render multiple title and subtitles if multiple tiles contain them", () => {
    sandbox.stub(window, "AWSendToDeviceEmailsSupported").resolves(true);
    const SECOND_TITLE_TILE = {
      type: "mobile_downloads",
      title: "Tile Title 2",
      subtitle: "Tile Subtitle 2",
      data: {
        email: {
          link_text: "Email yourself a link",
        },
      },
    };

    let MULTIPLE_TILES_CONTENT = {
      tiles: [TITLE_TILE, SECOND_TITLE_TILE],
    };

    const mountedWrapper = mount(
      <ContentTiles
        content={MULTIPLE_TILES_CONTENT}
        handleAction={() => {}}
        activeMultiSelect={null}
        setActiveMultiSelect={setActiveMultiSelect}
        setActiveSingleSelectSelection={setActiveSingleSelectSelection}
      />
    );

    const tileTitles = mountedWrapper.find(".tile-title");
    const tileSubtitles = mountedWrapper.find(".tile-subtitle");

    assert.equal(tileTitles.length, 2, "Should render two tile titles");
    assert.equal(tileSubtitles.length, 2, "Should render two tile subtitles");

    assert.equal(
      tileTitles.at(0).text(),
      "Tile Title",
      "First tile title should have correct text"
    );
    assert.equal(
      tileSubtitles.at(0).text(),
      "Tile Subtitle",
      "First tile subtitle should have correct text"
    );

    assert.equal(
      tileTitles.at(1).text(),
      "Tile Title 2",
      "Second tile title should have correct text"
    );
    assert.equal(
      tileSubtitles.at(1).text(),
      "Tile Subtitle 2",
      "Second tile subtitle should have correct text"
    );

    mountedWrapper.unmount();
  });

  it("should pre-populate multiple MultiSelect components in the same ContentTiles independently of one another", () => {
    const FIRST_MULTISELECT_TILE = {
      type: "multiselect",
      header: {
        title: "First Multiselect",
      },
      data: [
        { id: "checklist1-option1", defaultValue: true },
        { id: "checklist1-option2", defaultValue: false },
      ],
    };

    const SECOND_MULTISELECT_TILE = {
      type: "multiselect",
      header: {
        title: "Second Multiselect",
      },
      data: [
        { id: "checklist2-option1", defaultValue: false },
        { id: "checklist2-option2", defaultValue: true },
      ],
    };

    const contentWithMultipleMultiselects = {
      tiles: [FIRST_MULTISELECT_TILE, SECOND_MULTISELECT_TILE],
    };

    wrapper = mount(
      <ContentTiles
        content={contentWithMultipleMultiselects}
        activeMultiSelect={null}
        setActiveMultiSelect={setActiveMultiSelect}
        handleAction={handleAction}
      />
    );
    wrapper.update();

    sinon.assert.calledWithExactly(
      setActiveMultiSelect.getCall(0),
      ["checklist1-option1"],
      "tile-0"
    );
    sinon.assert.calledWithExactly(
      setActiveMultiSelect.getCall(1),
      ["checklist2-option2"],
      "tile-1"
    );

    wrapper.unmount();
  });

  it("should handle interaction between multiselect instances independently of one another", () => {
    const FIRST_MULTISELECT_TILE = {
      type: "multiselect",
      header: {
        title: "First Multiselect",
      },
      data: [
        { id: "checklist1-option1", defaultValue: true, label: "Option 1-1" },
        { id: "checklist1-option2", defaultValue: false, label: "Option 1-2" },
      ],
    };

    const SECOND_MULTISELECT_TILE = {
      type: "multiselect",
      header: {
        title: "Second Multiselect",
      },
      data: [
        { id: "checklist2-option1", defaultValue: false, label: "Option 2-1" },
        { id: "checklist2-option2", defaultValue: true, label: "Option 2-2" },
      ],
    };

    const contentWithMultipleMultiselects = {
      tiles: [FIRST_MULTISELECT_TILE, SECOND_MULTISELECT_TILE],
    };

    const initialActiveMultiSelect = {
      "tile-0": ["checklist1-option1"],
      "tile-1": ["checklist2-option2"],
    };

    const setScreenMultiSelects = sandbox.stub();

    wrapper = mount(
      <ContentTiles
        content={contentWithMultipleMultiselects}
        activeMultiSelect={initialActiveMultiSelect}
        setActiveMultiSelect={setActiveMultiSelect}
        setScreenMultiSelects={setScreenMultiSelects}
        handleAction={handleAction}
      />
    );

    // First multiselect
    const firstTileButton = wrapper.find(".tile-header").at(0);
    assert.ok(firstTileButton.exists(), "First tile header should exist");
    firstTileButton.simulate("click");

    const firstChecklistInputs = wrapper.find(".multi-select-container input");
    assert.equal(
      firstChecklistInputs.length,
      2,
      "First checklist should have 2 inputs"
    );

    assert.equal(
      firstChecklistInputs.at(0).prop("checked"),
      true,
      "First option should be checked"
    );
    assert.equal(
      firstChecklistInputs.at(1).prop("checked"),
      false,
      "Second option should be unchecked"
    );

    const secondInput = firstChecklistInputs.at(1);
    secondInput.getDOMNode().checked = true;
    secondInput.simulate("change");

    sinon.assert.calledOnce(setActiveMultiSelect);
    sinon.assert.calledWithExactly(
      setActiveMultiSelect,
      ["checklist1-option1", "checklist1-option2"],
      "tile-0"
    );
    setActiveMultiSelect.reset();

    // Second multiSelect
    const secondTileButton = wrapper.find(".tile-header").at(1);
    secondTileButton.simulate("click");

    const secondChecklistInputs = wrapper.find(".multi-select-container input");
    assert.equal(
      secondChecklistInputs.length,
      2,
      "Second checklist should have 2 inputs"
    );

    assert.equal(
      secondChecklistInputs.at(0).prop("checked"),
      false,
      "First option should be unchecked"
    );
    assert.equal(
      secondChecklistInputs.at(1).prop("checked"),
      true,
      "Second option should be checked"
    );

    // Uncheck the second checkbox in the second multiselect
    const lastInput = secondChecklistInputs.at(1);
    lastInput.getDOMNode().checked = false;
    lastInput.simulate("change");

    sinon.assert.calledOnce(setActiveMultiSelect);
    sinon.assert.calledWithExactly(setActiveMultiSelect, [], "tile-1");

    // Ensure the first multiselect's state wasn't altered by changes to the second multiselect
    wrapper.setProps({
      activeMultiSelect: {
        "tile-0": ["checklist1-option1", "checklist1-option2"],
        "tile-1": [],
      },
    });

    firstTileButton.simulate("click");

    // Get the updated checkboxes from the first checklist
    const updatedFirstInputs = wrapper.find(".multi-select-container input");

    assert.equal(
      updatedFirstInputs.at(0).prop("checked"),
      true,
      "First option should still be checked"
    );
    assert.equal(
      updatedFirstInputs.at(1).prop("checked"),
      true,
      "Second option should still be checked"
    );

    wrapper.unmount();
  });

  it("should select defaults of single select tiles independently of one another", () => {
    const SINGLE_SELECT_1 = {
      type: "single-select",
      selected: "test1",
      data: [
        {
          id: "test1",
          label: {
            raw: "test1 label",
          },
        },
        {
          defaultValue: true,
          id: "test2",
          label: {
            raw: "test2 label",
          },
        },
      ],
    };

    const SINGLE_SELECT_2 = {
      type: "single-select",
      selected: "test4",
      data: [
        {
          id: "test3",
          label: {
            raw: "test3 label",
          },
        },
        {
          defaultValue: true,
          id: "test4",
          label: {
            raw: "test4 label",
          },
        },
      ],
    };

    const content = { tiles: [SINGLE_SELECT_1, SINGLE_SELECT_2] };
    wrapper = mount(
      <ContentTiles
        content={content}
        setActiveSingleSelectSelection={setActiveSingleSelectSelection}
        handleAction={handleAction}
      />
    );
    wrapper.update();

    sinon.assert.calledWithExactly(
      setActiveSingleSelectSelection.getCall(0),
      "test1",
      "single-select-0"
    );

    sinon.assert.calledWithExactly(
      setActiveSingleSelectSelection.getCall(1),
      "test4",
      "single-select-1"
    );
    wrapper.unmount();
  });

  it("should handle interactions with multiple single select tiles independently of one another", () => {
    const SINGLE_SELECT_1 = {
      type: "single-select",
      selected: "test1",
      data: [
        {
          id: "test1",
          label: {
            raw: "test1 label",
          },
        },
        {
          defaultValue: true,
          id: "test2",
          label: {
            raw: "test2 label",
          },
        },
      ],
    };

    const SINGLE_SELECT_2 = {
      type: "single-select",
      selected: "test4",
      data: [
        {
          id: "test3",
          label: {
            raw: "test3 label",
          },
        },
        {
          defaultValue: true,
          id: "test4",
          label: {
            raw: "test4 label",
          },
        },
      ],
    };

    const content = { tiles: [SINGLE_SELECT_1, SINGLE_SELECT_2] };
    wrapper = mount(
      <ContentTiles
        content={content}
        setActiveSingleSelectSelection={setActiveSingleSelectSelection}
        handleAction={handleAction}
      />
    );

    wrapper.update();

    const tile2 = wrapper.find('input[value="test2"]');
    tile2.simulate("click");

    sinon.assert.calledWithExactly(
      setActiveSingleSelectSelection.getCall(2),
      "test2",
      "single-select-0"
    );

    const tile3 = wrapper.find('input[value="test3"]');
    tile3.simulate("click");

    sinon.assert.calledWithExactly(
      setActiveSingleSelectSelection.getCall(3),
      "test3",
      "single-select-1"
    );
    wrapper.unmount();
  });

  it("should apply styles to label element", () => {
    const TEST_STYLE = { marginBlock: "5px" };
    const tileData = {
      type: "single-select",
      selected: "vertical",
      data: [
        {
          id: "vertical",
          label: { raw: "Vertical", marginBlock: "5px" },
        },
      ],
    };

    wrapper = mount(
      <ContentTiles
        content={{ tiles: [tileData] }}
        setActiveSingleSelectSelection={() => {}}
        handleAction={() => {}}
      />
    );

    const styledDiv = wrapper.find(".text").at(0);

    assert.deepEqual(
      styledDiv.prop("style"),
      TEST_STYLE,
      "Style prop should match TEST_STYLE"
    );
    wrapper.unmount();
  });

  it("should apply valid styles from tile.data.style and include minWidth from icon.width", () => {
    const icon = {
      width: "101px",
    };

    const style = {
      paddingBlock: "8px",
    };

    const tileData = {
      type: "single-select",
      selected: "test",
      data: [
        {
          id: "test",
          icon,
          label: { raw: "Test" },
          style,
        },
      ],
    };

    wrapper = mount(
      <ContentTiles
        content={{ tiles: [tileData] }}
        setActiveSingleSelectSelection={() => {}}
        handleAction={() => {}}
      />
    );

    const label = wrapper.find("label.select-item").at(0);
    const labelStyle = label.prop("style");

    assert.equal(
      labelStyle.paddingBlock,
      "8px",
      "paddingBlock should be applied"
    );
    assert.equal(
      labelStyle.minWidth,
      "101px",
      "minWidth should be set from icon.width"
    );
    wrapper.unmount();
  });
});

it("restores last tiles focus in Spotlight context and genuine Tab is ignored", async () => {
  const TAB_GRACE_WINDOW_MS = 250;

  function nextFrame() {
    return new Promise(r => requestAnimationFrame(r));
  }
  function delay(ms) {
    return new Promise(r => setTimeout(r, ms));
  }
  async function waitFor(condition, timeout = TAB_GRACE_WINDOW_MS) {
    const start = performance.now();
    while (!condition()) {
      await nextFrame();
      if (performance.now() - start > timeout) {
        throw new Error("timeout waiting for condition");
      }
    }
  }

  // Pretend we're in a Spotlight dialog so the effect runs
  const root = document.body.appendChild(document.createElement("div"));
  root.id = "multi-stage-message-root";
  root.className = "onboardingContainer";
  root.dataset.page = "spotlight";

  const mountNode = document.body.appendChild(document.createElement("div"));

  const content = {
    tiles: [{ type: "multiselect", header: { title: "Test" }, data: [] }],
  };

  const wrapper = mount(
    <main role="alertdialog">
      <ContentTiles
        content={content}
        handleAction={() => {}}
        activeMultiSelect={null}
        setActiveMultiSelect={() => {}}
      />
      <div className="action-buttons">
        <button className="primary">Continue</button>
      </div>
    </main>,
    { attachTo: mountNode }
  );

  // Let the hook attach listeners
  await nextFrame();

  const dialog = wrapper.getDOMNode();
  const header = dialog.querySelector(".tile-header");
  const primary = dialog.querySelector(".action-buttons .primary");

  // Record real DOM focus inside tiles
  header.focus();
  header.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
  await nextFrame();

  // Wait past the tab grace window so this isn’t treated as a real Tab
  await delay(TAB_GRACE_WINDOW_MS + 1);

  // Simulate programmatic focus “snap” to an outside control
  primary.focus();
  primary.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));

  await waitFor(() => document.activeElement === header);
  assert.strictEqual(
    document.activeElement,
    header,
    "restored focus to tiles header"
  );

  dialog.dispatchEvent(
    new KeyboardEvent("keydown", {
      key: "Tab",
      bubbles: true,
      cancelable: true,
    })
  );
  primary.focus();
  primary.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
  await nextFrame();
  assert.strictEqual(
    document.activeElement,
    primary,
    "did not override genuine Tab focus"
  );

  // Cleanup
  wrapper.unmount();
  mountNode.remove();
  root.remove();
});
