import {
  actionCreators as ac,
  actionTypes as at,
} from "common/Actions.sys.mjs";
import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer";
import createMockRaf from "mock-raf";
import React from "react";

import { shallow } from "enzyme";

const perfSvc = {
  mark() {},
  getMostRecentAbsMarkStartByName() {},
};

let DEFAULT_PROPS = {
  initialized: true,
  rows: [],
  id: "highlights",
  dispatch() {},
  perfSvc,
};

describe("<ComponentPerfTimer>", () => {
  let mockRaf;
  let sandbox;
  let wrapper;

  const InnerEl = () => <div>Inner Element</div>;

  beforeEach(() => {
    mockRaf = createMockRaf();
    sandbox = sinon.createSandbox();
    sandbox.stub(window, "requestAnimationFrame").callsFake(mockRaf.raf);
    wrapper = shallow(
      <ComponentPerfTimer {...DEFAULT_PROPS}>
        <InnerEl />
      </ComponentPerfTimer>
    );
  });
  afterEach(() => {
    sandbox.restore();
  });

  it("should render props.children", () => {
    assert.ok(wrapper.contains(<InnerEl />));
  });

  describe("#constructor", () => {
    beforeEach(() => {
      sandbox.stub(ComponentPerfTimer.prototype, "_maybeSendBadStateEvent");
      sandbox.stub(
        ComponentPerfTimer.prototype,
        "_ensureFirstRenderTsRecorded"
      );
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>,
        { disableLifecycleMethods: true }
      );
    });

    it("should have the correct defaults", () => {
      const instance = wrapper.instance();

      assert.isFalse(instance._reportMissingData);
      assert.isFalse(instance._timestampHandled);
      assert.isFalse(instance._recordedFirstRender);
    });
  });

  describe("#render", () => {
    beforeEach(() => {
      sandbox.stub(DEFAULT_PROPS, "id").value("fake_section");
      sandbox.stub(ComponentPerfTimer.prototype, "_maybeSendBadStateEvent");
      sandbox.stub(
        ComponentPerfTimer.prototype,
        "_ensureFirstRenderTsRecorded"
      );
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );
    });

    it("should not call telemetry on sections that we don't want to record", () => {
      const instance = wrapper.instance();

      assert.notCalled(instance._maybeSendBadStateEvent);
      assert.notCalled(instance._ensureFirstRenderTsRecorded);
    });
  });

  describe("#_componentDidMount", () => {
    it("should call _maybeSendPaintedEvent", () => {
      const instance = wrapper.instance();
      const stub = sandbox.stub(instance, "_maybeSendPaintedEvent");

      instance.componentDidMount();

      assert.calledOnce(stub);
    });

    it("should not call _maybeSendPaintedEvent if id not in RECORDED_SECTIONS", () => {
      sandbox.stub(DEFAULT_PROPS, "id").value("topstories");
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );
      const instance = wrapper.instance();
      const stub = sandbox.stub(instance, "_maybeSendPaintedEvent");

      instance.componentDidMount();

      assert.notCalled(stub);
    });
  });

  describe("#_componentDidUpdate", () => {
    it("should call _maybeSendPaintedEvent", () => {
      const instance = wrapper.instance();
      const maybeSendPaintStub = sandbox.stub(
        instance,
        "_maybeSendPaintedEvent"
      );

      instance.componentDidUpdate();

      assert.calledOnce(maybeSendPaintStub);
    });

    it("should not call _maybeSendPaintedEvent if id not in RECORDED_SECTIONS", () => {
      sandbox.stub(DEFAULT_PROPS, "id").value("topstories");
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );
      const instance = wrapper.instance();
      const stub = sandbox.stub(instance, "_maybeSendPaintedEvent");

      instance.componentDidUpdate();

      assert.notCalled(stub);
    });
  });

  describe("_ensureFirstRenderTsRecorded", () => {
    let recordFirstRenderStub;
    beforeEach(() => {
      sandbox.stub(ComponentPerfTimer.prototype, "_maybeSendBadStateEvent");
      recordFirstRenderStub = sandbox.stub(
        ComponentPerfTimer.prototype,
        "_ensureFirstRenderTsRecorded"
      );
    });

    it("should set _recordedFirstRender", () => {
      sandbox.stub(DEFAULT_PROPS, "initialized").value(false);
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );
      const instance = wrapper.instance();

      assert.isFalse(instance._recordedFirstRender);

      recordFirstRenderStub.callThrough();
      instance._ensureFirstRenderTsRecorded();

      assert.isTrue(instance._recordedFirstRender);
    });

    it("should mark first_render_ts", () => {
      sandbox.stub(DEFAULT_PROPS, "initialized").value(false);
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );
      const instance = wrapper.instance();
      const stub = sandbox.stub(perfSvc, "mark");

      recordFirstRenderStub.callThrough();
      instance._ensureFirstRenderTsRecorded();

      assert.calledOnce(stub);
      assert.calledWithExactly(stub, `${DEFAULT_PROPS.id}_first_render_ts`);
    });
  });

  describe("#_maybeSendBadStateEvent", () => {
    let sendBadStateStub;
    beforeEach(() => {
      sendBadStateStub = sandbox.stub(
        ComponentPerfTimer.prototype,
        "_maybeSendBadStateEvent"
      );
      sandbox.stub(
        ComponentPerfTimer.prototype,
        "_ensureFirstRenderTsRecorded"
      );
    });

    it("should set this._reportMissingData=true when called with initialized === false", () => {
      sandbox.stub(DEFAULT_PROPS, "initialized").value(false);
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );
      const instance = wrapper.instance();

      assert.isFalse(instance._reportMissingData);

      sendBadStateStub.callThrough();
      instance._maybeSendBadStateEvent();

      assert.isTrue(instance._reportMissingData);
    });

    it("should call _sendBadStateEvent if initialized & other metrics have been recorded", () => {
      const instance = wrapper.instance();
      const stub = sandbox.stub(instance, "_sendBadStateEvent");
      instance._reportMissingData = true;
      instance._timestampHandled = true;
      instance._recordedFirstRender = true;

      sendBadStateStub.callThrough();
      instance._maybeSendBadStateEvent();

      assert.calledOnce(stub);
      assert.isFalse(instance._reportMissingData);
    });
  });

  describe("#_maybeSendPaintedEvent", () => {
    it("should call _sendPaintedEvent if props.initialized is true", () => {
      sandbox.stub(DEFAULT_PROPS, "initialized").value(true);
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>,
        { disableLifecycleMethods: true }
      );
      const instance = wrapper.instance();
      const stub = sandbox.stub(instance, "_afterFramePaint");

      assert.isFalse(instance._timestampHandled);

      instance._maybeSendPaintedEvent();

      assert.calledOnce(stub);
      assert.calledWithExactly(stub, instance._sendPaintedEvent);
      assert.isTrue(wrapper.instance()._timestampHandled);
    });
    it("should not call _sendPaintedEvent if this._timestampHandled is true", () => {
      const instance = wrapper.instance();
      const spy = sinon.spy(instance, "_afterFramePaint");
      instance._timestampHandled = true;

      instance._maybeSendPaintedEvent();
      spy.neverCalledWith(instance._sendPaintedEvent);
    });
    it("should not call _sendPaintedEvent if component not initialized", () => {
      sandbox.stub(DEFAULT_PROPS, "initialized").value(false);
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );
      const instance = wrapper.instance();
      const spy = sinon.spy(instance, "_afterFramePaint");

      instance._maybeSendPaintedEvent();

      spy.neverCalledWith(instance._sendPaintedEvent);
    });
  });

  describe("#_afterFramePaint", () => {
    it("should call callback after the requestAnimationFrame callback returns", () =>
      new Promise(resolve => {
        // Setting the callback to resolve is the test that it does finally get
        // called at the correct time, after the event loop ticks again.
        // If it doesn't get called, this test will time out.
        const callback = sandbox.spy(resolve);

        const instance = wrapper.instance();

        instance._afterFramePaint(callback);

        assert.notCalled(callback);
        mockRaf.step({ count: 1 });
      }));
  });

  describe("#_sendBadStateEvent", () => {
    it("should call perfSvc.mark", () => {
      sandbox.spy(perfSvc, "mark");
      const key = `${DEFAULT_PROPS.id}_data_ready_ts`;

      wrapper.instance()._sendBadStateEvent();

      assert.calledOnce(perfSvc.mark);
      assert.calledWithExactly(perfSvc.mark, key);
    });

    it("should call compute the delta from first render to data ready", () => {
      sandbox.stub(perfSvc, "getMostRecentAbsMarkStartByName");

      wrapper
        .instance()
        ._sendBadStateEvent(`${DEFAULT_PROPS.id}_data_ready_ts`);

      assert.calledTwice(perfSvc.getMostRecentAbsMarkStartByName);
      assert.calledWithExactly(
        perfSvc.getMostRecentAbsMarkStartByName,
        `${DEFAULT_PROPS.id}_data_ready_ts`
      );
      assert.calledWithExactly(
        perfSvc.getMostRecentAbsMarkStartByName,
        `${DEFAULT_PROPS.id}_first_render_ts`
      );
    });

    it("should call dispatch SAVE_SESSION_PERF_DATA", () => {
      sandbox
        .stub(perfSvc, "getMostRecentAbsMarkStartByName")
        .withArgs("highlights_first_render_ts")
        .returns(0.5)
        .withArgs("highlights_data_ready_ts")
        .returns(3.2);

      const dispatch = sandbox.spy(DEFAULT_PROPS, "dispatch");
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );

      wrapper.instance()._sendBadStateEvent();

      assert.calledOnce(dispatch);
      assert.calledWithExactly(
        dispatch,
        ac.OnlyToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: { [`${DEFAULT_PROPS.id}_data_late_by_ms`]: 2 },
        })
      );
    });
  });

  describe("#_sendPaintedEvent", () => {
    beforeEach(() => {
      sandbox.stub(ComponentPerfTimer.prototype, "_maybeSendBadStateEvent");
      sandbox.stub(
        ComponentPerfTimer.prototype,
        "_ensureFirstRenderTsRecorded"
      );
    });

    it("should not call mark with the wrong id", () => {
      sandbox.stub(perfSvc, "mark");
      sandbox.stub(DEFAULT_PROPS, "id").value("fake_id");
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );

      wrapper.instance()._sendPaintedEvent();

      assert.notCalled(perfSvc.mark);
    });
    it("should call mark with the correct topsites", () => {
      sandbox.stub(perfSvc, "mark");
      sandbox.stub(DEFAULT_PROPS, "id").value("topsites");
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );

      wrapper.instance()._sendPaintedEvent();

      assert.calledOnce(perfSvc.mark);
      assert.calledWithExactly(perfSvc.mark, "topsites_first_painted_ts");
    });
    it("should not call getMostRecentAbsMarkStartByName if id!=topsites", () => {
      sandbox.stub(perfSvc, "getMostRecentAbsMarkStartByName");
      sandbox.stub(DEFAULT_PROPS, "id").value("fake_id");
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );

      wrapper.instance()._sendPaintedEvent();

      assert.notCalled(perfSvc.getMostRecentAbsMarkStartByName);
    });
    it("should call getMostRecentAbsMarkStartByName for topsites", () => {
      sandbox.stub(perfSvc, "getMostRecentAbsMarkStartByName");
      sandbox.stub(DEFAULT_PROPS, "id").value("topsites");
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );

      wrapper.instance()._sendPaintedEvent();

      assert.calledOnce(perfSvc.getMostRecentAbsMarkStartByName);
      assert.calledWithExactly(
        perfSvc.getMostRecentAbsMarkStartByName,
        "topsites_first_painted_ts"
      );
    });
    it("should dispatch SAVE_SESSION_PERF_DATA", () => {
      sandbox.stub(perfSvc, "getMostRecentAbsMarkStartByName").returns(42);
      sandbox.stub(DEFAULT_PROPS, "id").value("topsites");
      const dispatch = sandbox.spy(DEFAULT_PROPS, "dispatch");
      wrapper = shallow(
        <ComponentPerfTimer {...DEFAULT_PROPS}>
          <InnerEl />
        </ComponentPerfTimer>
      );

      wrapper.instance()._sendPaintedEvent();

      assert.calledOnce(dispatch);
      assert.calledWithExactly(
        dispatch,
        ac.OnlyToMain({
          type: at.SAVE_SESSION_PERF_DATA,
          data: { topsites_first_painted_ts: 42 },
        })
      );
    });
  });
});
