import expect from 'expect';
import { spy, SinonSpy } from 'sinon';

import {
  LatencyTracker,
  LatencyState,
} from 'mweb/common/latency/latencyTracker';
import { LatencyReporter } from 'mweb/common/latency/latencyReporter';

class MockLatencyReporter {
  signalRenderingRoute: SinonSpy = spy();
  signalComponentInit: SinonSpy = spy();
  signalComponentInteractive: SinonSpy = spy();
  signalPageInteractive: SinonSpy = spy();
}

describe('LatencyTracker', () => {
  let mockReporter: MockLatencyReporter;
  function createTestTracker(
    componentID: string,
    parent: LatencyTracker | undefined,
  ): LatencyTracker {
    return new LatencyTracker(
      componentID,
      parent,
      (mockReporter as any) as LatencyReporter,
    );
  }

  beforeEach(() => {
    mockReporter = new MockLatencyReporter();
    LatencyTracker.pageCounter = 0;
    LatencyTracker.componentCounter = 0;
  });

  describe('constructor', () => {
    it('behaves as root when not given a parent and uses domLoading as start on first page', () => {
      const tracker = createTestTracker('Tracker', undefined);

      expect(tracker.latencyID).toEqual('Page0_0');
      expect(tracker.startMark).toEqual('domLoading');
      expect(mockReporter.signalComponentInit.called).toBeFalsy();
    });

    it('behaves as root when not given a parent and uses custom mark as start on second page', () => {
      LatencyTracker.pageCounter = 1;
      const tracker = createTestTracker('Tracker', undefined);

      expect(tracker.latencyID).toEqual('Page1_0');
      expect(tracker.startMark).toEqual('Page1_0_Tracker_init');
      expect(mockReporter.signalComponentInit.called).toBeFalsy();
    });

    it('behaves as child when given a parent and uses domLoading as start on first page', () => {
      const parent = createTestTracker('Parent', undefined);

      const child1 = createTestTracker('Child1', parent);
      expect(child1.latencyID).toEqual('Page0_0.0');
      expect(child1.startMark).toEqual('domLoading');

      const child2 = createTestTracker('Child2', parent);
      expect(child2.latencyID).toEqual('Page0_0.1');
      expect(child2.startMark).toEqual('domLoading');
    });

    it('behaves as child when given a parent and uses custom mark as start on second page', () => {
      LatencyTracker.pageCounter = 1;
      const parent = createTestTracker('Parent', undefined);

      const child1 = createTestTracker('Child1', parent);
      expect(child1.latencyID).toEqual('Page1_0.0');
      expect(child1.startMark).toEqual('Page1_0.0_Child1_init');

      const child2 = createTestTracker('Child2', parent);
      expect(child2.latencyID).toEqual('Page1_0.1');
      expect(child2.startMark).toEqual('Page1_0.1_Child2_init');

      expect(mockReporter.signalComponentInit.getCalls().length).toEqual(2);
    });
  });

  describe('reportInteractive', () => {
    it('does not double capture interactivity time', () => {
      const tracker = createTestTracker('Tracker', undefined);
      tracker.interactiveTime = 17;
      tracker.status = LatencyState.SelfInteractive;
      tracker.reportInteractive();

      expect(tracker.interactiveTime).toEqual(17);
    });

    it('sets end mark to domInteractive if interactive on mount on first page', () => {
      const tracker = createTestTracker('Tracker', undefined);
      tracker.reportInteractive(true);

      expect(tracker.endMark).toEqual('domInteractive');
      expect(tracker.interactiveTime).toExist();
    });

    it('creates its own endMark if interactive after mount on first page', () => {
      const tracker = createTestTracker('Tracker', undefined);
      tracker.reportInteractive();

      expect(tracker.endMark).toEqual('Page0_0_Tracker_interactive');
      expect(tracker.interactiveTime).toExist();
    });

    it('creates its own endMark if interactive on mount after first page', () => {
      LatencyTracker.pageCounter = 1;
      const tracker = createTestTracker('Tracker', undefined);
      tracker.reportInteractive(true);

      expect(tracker.endMark).toEqual('Page1_0_Tracker_interactive');
      expect(tracker.interactiveTime).toExist();
    });

    it('signals component interactive when given a parent and reports upstream', () => {
      const parent = createTestTracker('Parent', undefined);

      const child = createTestTracker('Child', parent);
      child.reportUpstream = spy();
      child.reportInteractive();

      expect(mockReporter.signalComponentInteractive.called).toBeTruthy();
      expect((child.reportUpstream as SinonSpy).called).toBeTruthy();
    });
  });

  describe('registerChild', () => {
    it('adds the child to the parent and returns the id for the newly added child', () => {
      const parent = createTestTracker('Parent', undefined);
      const child = createTestTracker('Child', undefined);

      expect(parent.children.length).toEqual(0);
      expect(parent.registerChild(child)).toEqual('Page0_0.0');
      expect(parent.children.length).toEqual(1);
      expect(parent.children[0]).toEqual(child);
    });
  });

  describe('unregisterChild', () => {
    let parent: LatencyTracker;
    let child: LatencyTracker;

    beforeEach(() => {
      parent = createTestTracker('Parent', undefined);
      child = createTestTracker('Child', parent);
    });

    it('removes a child that is registered', () => {
      expect(parent.children.length).toEqual(1);
      expect(parent.children[0]).toEqual(child);
      parent.unregisterChild(child);
      expect(parent.children.length).toEqual(0);
    });

    it('does nothing when given a non-child tracker', () => {
      expect(parent.children.length).toEqual(1);
      expect(parent.children[0]).toEqual(child);

      const notChild = createTestTracker('NotChild', undefined);
      parent.unregisterChild(notChild);

      expect(parent.children.length).toEqual(1);
      expect(parent.children[0]).toEqual(child);
    });
  });

  describe('reportInteractiveChild', () => {
    it('attempts to report upstream', () => {
      const tracker = createTestTracker('Tracker', undefined);
      tracker.reportUpstream = spy();
      tracker.reportInteractiveChild();

      expect((tracker.reportUpstream as SinonSpy).called).toBeTruthy();
    });
  });

  describe('areAllChildrenInteractive', () => {
    let parent: LatencyTracker;
    let child1: LatencyTracker;
    let child2: LatencyTracker;

    beforeEach(() => {
      parent = createTestTracker('Parent', undefined);
      child1 = createTestTracker('Child1', parent);
      child1.status = LatencyState.SelfAndSubtreeInteractive;
      child2 = createTestTracker('Child2', parent);
      child2.status = LatencyState.SelfAndSubtreeInteractive;
    });

    it('is true when all children have interactive self and subtree', () => {
      expect(parent.areAllChildrenInteractive).toEqual(true);
    });

    it('is true when there are no children', () => {
      parent.children = [];
      expect(parent.areAllChildrenInteractive).toEqual(true);
    });

    it('is false when only some children have interactive self and subtree', () => {
      child2.status = LatencyState.SelfInteractive;
      expect(parent.areAllChildrenInteractive).toEqual(false);
    });

    it('is false when all children are in self interactive status', () => {
      child1.status = LatencyState.SelfInteractive;
      child2.status = LatencyState.SelfInteractive;
      expect(parent.areAllChildrenInteractive).toEqual(false);
    });

    it('is false when all children are in not interactive status', () => {
      child1.status = LatencyState.NotInteractive;
      child2.status = LatencyState.NotInteractive;
      expect(parent.areAllChildrenInteractive).toEqual(false);
    });
  });

  describe('maxTimeToInteractive', () => {
    let parent: LatencyTracker;
    let child1: LatencyTracker;
    let child2: LatencyTracker;
    let grandchild11: LatencyTracker;
    let grandchild12: LatencyTracker;
    let grandchild21: LatencyTracker;
    let grandchild22: LatencyTracker;

    beforeEach(() => {
      parent = createTestTracker('Parent', undefined);
      parent.interactiveTime = 1;
      parent.endMark = 'Parent';

      child1 = createTestTracker('Child1', parent);
      child1.interactiveTime = 1;
      child1.endMark = 'Child1';
      grandchild11 = createTestTracker('Grandchild11', child1);
      grandchild11.interactiveTime = 1;
      grandchild11.endMark = 'Grandchild11';
      grandchild12 = createTestTracker('Grandchild12', child1);
      grandchild12.interactiveTime = 1;
      grandchild12.endMark = 'Grandchild12';

      child2 = createTestTracker('Child2', parent);
      child2.interactiveTime = 1;
      child2.endMark = 'Child2';
      grandchild21 = createTestTracker('Grandchild21', child2);
      grandchild21.interactiveTime = 1;
      grandchild21.endMark = 'Grandchild21';
      grandchild22 = createTestTracker('Grandchild22', child2);
      grandchild22.interactiveTime = 1;
      grandchild22.endMark = 'Grandchild22';
    });

    it('reports own time when there are no children', () => {
      parent.children = [];
      parent.interactiveTime = 2.1;
      expect(parent.maxInteractiveTime).toEqual({ time: 2.1, mark: 'Parent' });
    });

    it('properly reports first child time when appropriate', () => {
      child1.interactiveTime = 3.2;
      expect(parent.maxInteractiveTime).toEqual({ time: 3.2, mark: 'Child1' });
    });

    it('properly reports last child time when appropriate', () => {
      child2.interactiveTime = 4.3;
      expect(parent.maxInteractiveTime).toEqual({ time: 4.3, mark: 'Child2' });
    });

    it("properly reports grandchild (first child's first child) time when appropriate", () => {
      grandchild11.interactiveTime = 5.4;
      expect(parent.maxInteractiveTime).toEqual({
        time: 5.4,
        mark: 'Grandchild11',
      });
    });

    it("properly reports grandchild (first child's last child) time when appropriate", () => {
      grandchild12.interactiveTime = 6.5;
      expect(parent.maxInteractiveTime).toEqual({
        time: 6.5,
        mark: 'Grandchild12',
      });
    });

    it("properly reports grandchild (last child's first child) time when appropriate", () => {
      grandchild21.interactiveTime = 7.6;
      expect(parent.maxInteractiveTime).toEqual({
        time: 7.6,
        mark: 'Grandchild21',
      });
    });

    it("properly reports grandchild (last child's last child) time when appropriate", () => {
      grandchild22.interactiveTime = 8.7;
      expect(parent.maxInteractiveTime).toEqual({
        time: 8.7,
        mark: 'Grandchild22',
      });
    });
  });

  describe('reportUpstream', () => {
    it('does not do anything if status is not self interactive', () => {
      const tracker = createTestTracker('Tracker', undefined);
      tracker.reportUpstream();

      expect(tracker.status).toEqual(LatencyState.NotInteractive);
    });

    it('does not do anything if children are not interactive', () => {
      const parent = createTestTracker('Parent', undefined);
      parent.status = LatencyState.SelfInteractive;
      createTestTracker('Child', parent);
      parent.reportUpstream();

      expect(parent.status).toEqual(LatencyState.SelfInteractive);
    });

    it('sets self and sub interactive and reports interactive to parent if child', () => {
      const parent = createTestTracker('Parent', undefined);
      parent.status = LatencyState.SelfInteractive;
      parent.reportInteractiveChild = spy();
      const child = createTestTracker('Child', parent);
      child.status = LatencyState.SelfInteractive;
      child.reportUpstream();

      expect(child.status).toEqual(LatencyState.SelfAndSubtreeInteractive);
      expect((parent.reportInteractiveChild as SinonSpy).called).toBeTruthy();
    });

    it('sets self and sub interactive and signals signalPageInteractive if root', () => {
      const tracker = createTestTracker('Tracker', undefined);
      tracker.reportInteractive(); // triggers reportUpstream whilst setting endMark properly

      expect(tracker.status).toEqual(LatencyState.SelfAndSubtreeInteractive);
      expect(
        (mockReporter.signalPageInteractive as SinonSpy).called,
      ).toBeTruthy();
    });
  });

  describe('destroy', () => {
    let parent: LatencyTracker;
    let child: LatencyTracker;

    beforeEach(() => {
      parent = createTestTracker('Parent', undefined);
      child = createTestTracker('Child', parent);
    });

    it('resets it children array', () => {
      parent.destroy();
      expect(parent.children.length).toEqual(0);
    });

    it('unregisters itself from parent', () => {
      child.destroy();
      expect(parent.children.length).toEqual(0);
    });
  });

  it('resetForNextPage resets properties, root increments page, and child re-registers with parent', () => {
    const parent = createTestTracker('Parent', undefined);
    parent.status = LatencyState.SelfAndSubtreeInteractive;
    parent.endMark = 'done';
    parent.interactiveTime = 1;

    const child = createTestTracker('Child', parent);
    child.status = LatencyState.SelfAndSubtreeInteractive;
    child.endMark = 'done';
    child.interactiveTime = 1;

    expect(parent.latencyID).toEqual('Page0_0');
    expect(parent.latencyID).toEqual('Page0_0');
    expect(child.startMark).toEqual('domLoading');
    expect(child.startMark).toEqual('domLoading');
    expect(LatencyTracker.pageCounter).toEqual(0);

    parent.resetForNextPage();

    expect(parent.status).toEqual(LatencyState.NotInteractive);
    expect(parent.children.length).toEqual(0);
    expect(parent.endMark).toNotExist();
    expect(parent.interactiveTime).toNotExist();
    expect(parent.latencyID).toEqual('Page1_0');
    expect(parent.startMark).toEqual('Page1_0_Parent_init');
    expect(mockReporter.signalRenderingRoute.called).toBeTruthy();
    expect(LatencyTracker.pageCounter).toEqual(1);

    child.resetForNextPage();

    expect(child.status).toEqual(LatencyState.NotInteractive);
    expect(child.endMark).toNotExist();
    expect(child.interactiveTime).toNotExist();
    expect(child.latencyID).toEqual('Page1_0.0');
    expect(child.startMark).toEqual('Page1_0.0_Child_init');

    expect(parent.children.length).toEqual(1);
    expect(parent.children[0]).toEqual(child);
  });
});
