import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import {
  _Card as Card,
  PlaceholderCard,
} from "content-src/components/Card/Card";
import { combineReducers, createStore } from "redux";
import { GlobalOverrider } from "test/unit/utils";
import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
import { cardContextTypes } from "content-src/components/Card/types";
import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
import { Provider } from "react-redux";
import React from "react";
import { shallow, mount } from "enzyme";

let DEFAULT_PROPS = {
  dispatch: sinon.stub(),
  index: 0,
  link: {
    hostname: "foo",
    title: "A title for foo",
    url: "http://www.foo.com",
    type: "history",
    description: "A description for foo",
    image: "http://www.foo.com/img.png",
    guid: 1,
  },
  eventSource: "TOP_STORIES",
  shouldSendImpressionStats: true,
  contextMenuOptions: ["Separator"],
};

let DEFAULT_BLOB_IMAGE = {
  path: "/testpath",
  data: new Blob([0]),
};

function mountCardWithProps(props) {
  const store = createStore(combineReducers(reducers), INITIAL_STATE);
  return mount(
    <Provider store={store}>
      <Card {...props} />
    </Provider>
  );
}

describe("<Card>", () => {
  let globals;
  let wrapper;
  beforeEach(() => {
    globals = new GlobalOverrider();
    wrapper = mountCardWithProps(DEFAULT_PROPS);
  });
  afterEach(() => {
    DEFAULT_PROPS.dispatch.reset();
    globals.restore();
  });
  it("should render a Card component", () => assert.ok(wrapper.exists()));
  it("should add the right url", () => {
    assert.propertyVal(
      wrapper.find("a").props(),
      "href",
      DEFAULT_PROPS.link.url
    );

    // test that pocket cards get a special open_url href
    const pocketLink = Object.assign({}, DEFAULT_PROPS.link, {
      open_url: "getpocket.com/foo",
      type: "pocket",
    });
    wrapper = mount(
      <Card {...Object.assign({}, DEFAULT_PROPS, { link: pocketLink })} />
    );
    assert.propertyVal(wrapper.find("a").props(), "href", pocketLink.open_url);
  });
  it("should display a title", () =>
    assert.equal(wrapper.find(".card-title").text(), DEFAULT_PROPS.link.title));
  it("should display a description", () =>
    assert.equal(
      wrapper.find(".card-description").text(),
      DEFAULT_PROPS.link.description
    ));
  it("should display a host name", () =>
    assert.equal(wrapper.find(".card-host-name").text(), "foo"));
  it("should have a link menu button", () =>
    assert.ok(wrapper.find(".context-menu-button").exists()));
  it("should render a link menu when button is clicked", () => {
    const button = wrapper.find(".context-menu-button");
    assert.equal(wrapper.find(LinkMenu).length, 0);
    button.simulate("click", { preventDefault: () => {} });
    assert.equal(wrapper.find(LinkMenu).length, 1);
  });
  it("should pass dispatch, source, onUpdate, site, options, and index to LinkMenu", () => {
    wrapper
      .find(".context-menu-button")
      .simulate("click", { preventDefault: () => {} });
    // eslint-disable-next-line no-shadow
    const { dispatch, source, onUpdate, site, options, index } = wrapper
      .find(LinkMenu)
      .props();
    assert.equal(dispatch, DEFAULT_PROPS.dispatch);
    assert.equal(source, DEFAULT_PROPS.eventSource);
    assert.ok(onUpdate);
    assert.equal(site, DEFAULT_PROPS.link);
    assert.equal(options, DEFAULT_PROPS.contextMenuOptions);
    assert.equal(index, DEFAULT_PROPS.index);
  });
  it("should pass through the correct menu options to LinkMenu if overridden by individual card", () => {
    const link = Object.assign({}, DEFAULT_PROPS.link);
    link.contextMenuOptions = ["CheckBookmark"];

    wrapper = mountCardWithProps(Object.assign({}, DEFAULT_PROPS, { link }));
    wrapper
      .find(".context-menu-button")
      .simulate("click", { preventDefault: () => {} });
    // eslint-disable-next-line no-shadow
    const { options } = wrapper.find(LinkMenu).props();
    assert.equal(options, link.contextMenuOptions);
  });
  it("should have a context based on type", () => {
    wrapper = shallow(<Card {...DEFAULT_PROPS} />);
    const cardContext = wrapper.find(".card-context");
    const { icon, fluentID } = cardContextTypes[DEFAULT_PROPS.link.type];
    assert.isTrue(cardContext.childAt(0).hasClass(`icon-${icon}`));
    assert.isTrue(cardContext.childAt(1).hasClass("card-context-label"));
    assert.equal(cardContext.childAt(1).prop("data-l10n-id"), fluentID);
  });
  it("should support setting custom context", () => {
    const linkWithCustomContext = {
      type: "history",
      context: "Custom",
      icon: "icon-url",
    };

    wrapper = shallow(
      <Card
        {...Object.assign({}, DEFAULT_PROPS, { link: linkWithCustomContext })}
      />
    );
    const cardContext = wrapper.find(".card-context");
    const { icon } = cardContextTypes[DEFAULT_PROPS.link.type];
    assert.isFalse(cardContext.childAt(0).hasClass(`icon-${icon}`));
    assert.equal(
      cardContext.childAt(0).props().style.backgroundImage,
      "url('icon-url')"
    );

    assert.isTrue(cardContext.childAt(1).hasClass("card-context-label"));
    assert.equal(cardContext.childAt(1).text(), linkWithCustomContext.context);
  });
  it("should parse args for fluent correctly", () => {
    const title = '"fluent"';
    const link = { ...DEFAULT_PROPS.link, title };

    wrapper = mountCardWithProps({ ...DEFAULT_PROPS, link });
    let button = wrapper.find(ContextMenuButton).find("button");

    assert.equal(button.prop("data-l10n-args"), JSON.stringify({ title }));
  });
  it("should have .active class, on card-outer if context menu is open", () => {
    const button = wrapper.find(ContextMenuButton);
    assert.isFalse(
      wrapper.find(".card-outer").hasClass("active"),
      "does not have active class"
    );
    button.simulate("click", { preventDefault: () => {} });
    assert.isTrue(
      wrapper.find(".card-outer").hasClass("active"),
      "has active class"
    );
  });
  it("should send OPEN_DOWNLOAD_FILE if we clicked on a download", () => {
    const downloadLink = {
      type: "download",
      url: "download.mov",
    };
    wrapper = mountCardWithProps(
      Object.assign({}, DEFAULT_PROPS, { link: downloadLink })
    );
    const card = wrapper.find(".card");
    card.simulate("click", { preventDefault: () => {} });
    assert.calledThrice(DEFAULT_PROPS.dispatch);

    assert.equal(
      DEFAULT_PROPS.dispatch.firstCall.args[0].type,
      at.OPEN_DOWNLOAD_FILE
    );
    assert.deepEqual(
      DEFAULT_PROPS.dispatch.firstCall.args[0].data,
      downloadLink
    );
  });
  it("should send OPEN_LINK if we clicked on anything other than a download", () => {
    const nonDownloadLink = {
      type: "history",
      url: "download.mov",
    };
    wrapper = mountCardWithProps(
      Object.assign({}, DEFAULT_PROPS, { link: nonDownloadLink })
    );
    const card = wrapper.find(".card");
    const event = {
      altKey: "1",
      button: "2",
      ctrlKey: "3",
      metaKey: "4",
      shiftKey: "5",
    };
    card.simulate(
      "click",
      Object.assign({}, event, { preventDefault: () => {} })
    );
    assert.calledThrice(DEFAULT_PROPS.dispatch);

    assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK);
  });
  describe("card image display", () => {
    const DEFAULT_BLOB_URL = "blob://test";
    let url;
    beforeEach(() => {
      url = {
        createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL),
        revokeObjectURL: globals.sandbox.spy(),
      };
      globals.set("URL", url);
    });
    afterEach(() => {
      globals.restore();
    });
    it("should display a regular image correctly and not call revokeObjectURL when unmounted", () => {
      wrapper = shallow(<Card {...DEFAULT_PROPS} />);

      assert.isUndefined(wrapper.state("cardImage").path);
      assert.equal(wrapper.state("cardImage").url, DEFAULT_PROPS.link.image);
      assert.equal(
        wrapper.find(".card-preview-image").props().style.backgroundImage,
        `url(${wrapper.state("cardImage").url})`
      );

      wrapper.unmount();
      assert.notCalled(url.revokeObjectURL);
    });
    it("should display a blob image correctly and revoke blob url when unmounted", () => {
      const link = Object.assign({}, DEFAULT_PROPS.link, {
        image: DEFAULT_BLOB_IMAGE,
      });
      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);

      assert.equal(wrapper.state("cardImage").path, DEFAULT_BLOB_IMAGE.path);
      assert.equal(wrapper.state("cardImage").url, DEFAULT_BLOB_URL);
      assert.equal(
        wrapper.find(".card-preview-image").props().style.backgroundImage,
        `url(${wrapper.state("cardImage").url})`
      );

      wrapper.unmount();
      assert.calledOnce(url.revokeObjectURL);
    });
    it("should not show an image if there isn't one and not call revokeObjectURL when unmounted", () => {
      const link = Object.assign({}, DEFAULT_PROPS.link);
      delete link.image;

      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);

      assert.isNull(wrapper.state("cardImage"));
      assert.lengthOf(wrapper.find(".card-preview-image"), 0);

      wrapper.unmount();
      assert.notCalled(url.revokeObjectURL);
    });
    it("should remove current card image if new image is not present", () => {
      wrapper = shallow(<Card {...DEFAULT_PROPS} />);

      const otherLink = Object.assign({}, DEFAULT_PROPS.link);
      delete otherLink.image;
      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));

      assert.isNull(wrapper.state("cardImage"));
    });
    it("should not create or revoke urls if normal image is already in state", () => {
      wrapper = shallow(<Card {...DEFAULT_PROPS} />);

      wrapper.setProps(DEFAULT_PROPS);

      assert.notCalled(url.createObjectURL);
      assert.notCalled(url.revokeObjectURL);
    });
    it("should not create or revoke more urls if blob image is already in state", () => {
      const link = Object.assign({}, DEFAULT_PROPS.link, {
        image: DEFAULT_BLOB_IMAGE,
      });
      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);

      assert.calledOnce(url.createObjectURL);
      assert.notCalled(url.revokeObjectURL);

      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link }));

      assert.calledOnce(url.createObjectURL);
      assert.notCalled(url.revokeObjectURL);
    });
    it("should create blob urls for new blobs and revoke existing ones", () => {
      const link = Object.assign({}, DEFAULT_PROPS.link, {
        image: DEFAULT_BLOB_IMAGE,
      });
      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);

      assert.calledOnce(url.createObjectURL);
      assert.notCalled(url.revokeObjectURL);

      const otherLink = Object.assign({}, DEFAULT_PROPS.link, {
        image: { path: "/newpath", data: new Blob([0]) },
      });
      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));

      assert.calledTwice(url.createObjectURL);
      assert.calledOnce(url.revokeObjectURL);
    });
    it("should not call createObjectURL and revokeObjectURL for normal images", () => {
      wrapper = shallow(<Card {...DEFAULT_PROPS} />);

      assert.notCalled(url.createObjectURL);
      assert.notCalled(url.revokeObjectURL);

      const otherLink = Object.assign({}, DEFAULT_PROPS.link, {
        image: "https://other/image",
      });
      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));

      assert.notCalled(url.createObjectURL);
      assert.notCalled(url.revokeObjectURL);
    });
  });
  describe("image loading", () => {
    let link;
    let triggerImage = {};
    let uniqueLink = 0;
    beforeEach(() => {
      global.Image.prototype = {
        addEventListener(event, callback) {
          triggerImage[event] = () => Promise.resolve(callback());
        },
      };

      link = Object.assign({}, DEFAULT_PROPS.link);
      link.image += uniqueLink++;
      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);
    });
    it("should have a loaded preview image when the image is loaded", () => {
      assert.isFalse(wrapper.find(".card-preview-image").hasClass("loaded"));

      wrapper.setState({ imageLoaded: true });

      assert.isTrue(wrapper.find(".card-preview-image").hasClass("loaded"));
    });
    it("should start not loaded", () => {
      assert.isFalse(wrapper.state("imageLoaded"));
    });
    it("should be loaded after load", async () => {
      await triggerImage.load();

      assert.isTrue(wrapper.state("imageLoaded"));
    });
    it("should be not be loaded after error ", async () => {
      await triggerImage.error();

      assert.isFalse(wrapper.state("imageLoaded"));
    });
    it("should be not be loaded if image changes", async () => {
      await triggerImage.load();
      const otherLink = Object.assign({}, link, {
        image: "https://other/image",
      });

      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));

      assert.isFalse(wrapper.state("imageLoaded"));
    });
  });
  describe("placeholder=true", () => {
    beforeEach(() => {
      wrapper = mount(<Card placeholder={true} />);
    });
    it("should render when placeholder=true", () => {
      assert.ok(wrapper.exists());
    });
    it("should add a placeholder class to the outer element", () => {
      assert.isTrue(wrapper.find(".card-outer").hasClass("placeholder"));
    });
    it("should not have a context menu button or LinkMenu", () => {
      assert.isFalse(
        wrapper.find(ContextMenuButton).exists(),
        "context menu button"
      );
      assert.isFalse(wrapper.find(LinkMenu).exists(), "LinkMenu");
    });
    it("should not call onLinkClick when the link is clicked", () => {
      const spy = sinon.spy(wrapper.instance(), "onLinkClick");
      const card = wrapper.find(".card");
      card.simulate("click");
      assert.notCalled(spy);
    });
  });
  describe("#trackClick", () => {
    it("should call dispatch when the link is clicked with the right data", () => {
      const card = wrapper.find(".card");
      const event = {
        altKey: "1",
        button: "2",
        ctrlKey: "3",
        metaKey: "4",
        shiftKey: "5",
      };
      card.simulate(
        "click",
        Object.assign({}, event, { preventDefault: () => {} })
      );
      assert.calledThrice(DEFAULT_PROPS.dispatch);

      // first dispatch call is the AlsoToMain message which will open a link in a window, and send some event data
      assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK);
      assert.deepEqual(
        DEFAULT_PROPS.dispatch.firstCall.args[0].data.event,
        event
      );

      // second dispatch call is a UserEvent action for telemetry
      assert.isUserEventAction(DEFAULT_PROPS.dispatch.secondCall.args[0]);
      assert.calledWith(
        DEFAULT_PROPS.dispatch.secondCall,
        ac.UserEvent({
          event: "CLICK",
          source: DEFAULT_PROPS.eventSource,
          action_position: DEFAULT_PROPS.index,
        })
      );

      // third dispatch call is to send impression stats
      assert.calledWith(
        DEFAULT_PROPS.dispatch.thirdCall,
        ac.ImpressionStats({
          source: DEFAULT_PROPS.eventSource,
          click: 0,
          tiles: [{ id: DEFAULT_PROPS.link.guid, pos: DEFAULT_PROPS.index }],
        })
      );
    });
    it("should provide card_type to telemetry info if type is not history", () => {
      const link = Object.assign({}, DEFAULT_PROPS.link);
      link.type = "bookmark";
      wrapper = mount(<Card {...Object.assign({}, DEFAULT_PROPS, { link })} />);
      const card = wrapper.find(".card");
      const event = {
        altKey: "1",
        button: "2",
        ctrlKey: "3",
        metaKey: "4",
        shiftKey: "5",
      };

      card.simulate(
        "click",
        Object.assign({}, event, { preventDefault: () => {} })
      );

      assert.isUserEventAction(DEFAULT_PROPS.dispatch.secondCall.args[0]);
      assert.calledWith(
        DEFAULT_PROPS.dispatch.secondCall,
        ac.UserEvent({
          event: "CLICK",
          source: DEFAULT_PROPS.eventSource,
          action_position: DEFAULT_PROPS.index,
          value: { card_type: link.type },
        })
      );
    });
    it("should notify Web Extensions with WEBEXT_CLICK if props.isWebExtension is true", () => {
      wrapper = mountCardWithProps(
        Object.assign({}, DEFAULT_PROPS, {
          isWebExtension: true,
          eventSource: "MyExtension",
          index: 3,
        })
      );
      const card = wrapper.find(".card");
      const event = { preventDefault() {} };
      card.simulate("click", event);
      assert.calledWith(
        DEFAULT_PROPS.dispatch,
        ac.WebExtEvent(at.WEBEXT_CLICK, {
          source: "MyExtension",
          url: DEFAULT_PROPS.link.url,
          action_position: 3,
        })
      );
    });
  });
});

describe("<PlaceholderCard />", () => {
  it("should render a Card with placeholder=true", () => {
    const wrapper = mount(
      <Provider store={createStore(combineReducers(reducers), INITIAL_STATE)}>
        <PlaceholderCard />
      </Provider>
    );
    assert.isTrue(wrapper.find(Card).props().placeholder);
  });
});
