// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {microsecondsTraceWindow} from '../../../testing/TraceHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as Trace from '../trace.js';

function milliToMicro(value: number) {
  return Trace.Types.Timing.Micro(value * 1000);
}

describeWithEnvironment('Timing helpers', () => {
  describe('Timing conversions', () => {
    it('can convert milliseconds to microseconds', () => {
      const input = Trace.Types.Timing.Milli(1);
      const expected = Trace.Types.Timing.Micro(1000);
      assert.strictEqual(Trace.Helpers.Timing.milliToMicro(input), expected);
    });

    it('can convert seconds to milliseconds', () => {
      const input = Trace.Types.Timing.Seconds(1);
      const expected = Trace.Types.Timing.Milli(1000);
      assert.strictEqual(Trace.Helpers.Timing.secondsToMilli(input), expected);
    });

    it('can convert seconds to microseconds', () => {
      const input = Trace.Types.Timing.Seconds(1);
      // 1 Second = 1000 Milliseconds
      // 1000 Milliseconds = 1,000,000 Microseconds
      const expected = Trace.Types.Timing.Micro(1_000_000);
      assert.strictEqual(Trace.Helpers.Timing.secondsToMicro(input), expected);
    });

    it('can convert microSeconds milliseconds', () => {
      const input = Trace.Types.Timing.Micro(1_000_000);
      const expected = Trace.Types.Timing.Milli(1_000);
      assert.strictEqual(Trace.Helpers.Timing.microToMilli(input), expected);
    });
  });

  it('eventTimingsMicroSeconds returns the right numbers', async () => {
    const event = {
      ts: 10,
      dur: 5,
    } as unknown as Trace.Types.Events.Event;
    assert.deepEqual(Trace.Helpers.Timing.eventTimingsMicroSeconds(event), {
      startTime: Trace.Types.Timing.Micro(10),
      endTime: Trace.Types.Timing.Micro(15),
      duration: Trace.Types.Timing.Micro(5),
    });
  });

  it('eventTimingsMilliSeconds returns the right numbers', async () => {
    const event = {
      ts: 10_000,
      dur: 5_000,
    } as unknown as Trace.Types.Events.Event;
    assert.deepEqual(Trace.Helpers.Timing.eventTimingsMilliSeconds(event), {
      startTime: Trace.Types.Timing.Milli(10),
      endTime: Trace.Types.Timing.Milli(15),
      duration: Trace.Types.Timing.Milli(5),
    });
  });

  describe('timeStampForEventAdjustedByClosestNavigation', () => {
    it('can use the navigation ID to adjust the time correctly', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const lcpEvent = parsedTrace.PageLoadMetrics.allMarkerEvents.find(event => {
        // Just one LCP Event so we do not need to worry about ordering and finding the right one.
        return event.name === 'largestContentfulPaint::Candidate';
      });
      if (!lcpEvent) {
        throw new Error('Could not find LCP event');
      }
      // Ensure we are testing the navigationID path!
      assert.exists(lcpEvent.args.data?.navigationId);
      const adjustedTime = Trace.Helpers.Timing.timeStampForEventAdjustedByClosestNavigation(
          lcpEvent,
          parsedTrace.Meta.traceBounds,
          parsedTrace.Meta.navigationsByNavigationId,
          parsedTrace.Meta.navigationsByFrameId,
      );

      const unadjustedTime = Trace.Helpers.Timing.microToMilli(
          Trace.Types.Timing.Micro(lcpEvent.ts - parsedTrace.Meta.traceBounds.min),
      );
      assert.strictEqual(unadjustedTime.toFixed(2), String(130.31));

      // To make the assertion easier to read.
      const timeAsMS = Trace.Helpers.Timing.microToMilli(adjustedTime);
      assert.strictEqual(timeAsMS.toFixed(2), String(118.44));
    });

    it('can use the frame ID to adjust the time correctly', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const dclEvent = parsedTrace.PageLoadMetrics.allMarkerEvents.find(event => {
        return event.name === 'MarkDOMContent' && event.args.data?.frame === parsedTrace.Meta.mainFrameId;
      });
      if (!dclEvent) {
        throw new Error('Could not find DCL event');
      }
      // Ensure we are testing the frameID path!
      assert.isUndefined(dclEvent.args.data?.navigationId);

      const unadjustedTime = Trace.Helpers.Timing.microToMilli(
          Trace.Types.Timing.Micro(dclEvent.ts - parsedTrace.Meta.traceBounds.min),
      );
      assert.strictEqual(unadjustedTime.toFixed(2), String(190.79));
      const adjustedTime = Trace.Helpers.Timing.timeStampForEventAdjustedByClosestNavigation(
          dclEvent,
          parsedTrace.Meta.traceBounds,
          parsedTrace.Meta.navigationsByNavigationId,
          parsedTrace.Meta.navigationsByFrameId,
      );

      // To make the assertion easier to read.
      const timeAsMS = Trace.Helpers.Timing.microToMilli(adjustedTime);
      assert.strictEqual(timeAsMS.toFixed(2), String(178.92));
    });
  });

  describe('expandWindowByPercentOrToOneMillisecond', () => {
    it('can expand trace window by a percentage', async function() {
      const traceWindow = Trace.Helpers.Timing.traceWindowFromMicroSeconds(
          milliToMicro(40),
          milliToMicro(60),
      );

      const maxTraceWindow = Trace.Helpers.Timing.traceWindowFromMicroSeconds(
          milliToMicro(0),
          milliToMicro(100),
      );

      const expandedTraceWindow =
          Trace.Helpers.Timing.expandWindowByPercentOrToOneMillisecond(traceWindow, maxTraceWindow, 50);

      // Since initial window was 20ms, make sure the that it is 30ms after being expanded by 50%
      assert.strictEqual(expandedTraceWindow.range, 30000);
      // min and max bounds are expanded by 25% from the initial window bounds
      assert.strictEqual(expandedTraceWindow.min, 35000);
      assert.strictEqual(expandedTraceWindow.max, 65000);
    });

    it('if the expanded window is smaller than 1 millisecond, expands it to 1 millisecond ', async function() {
      // Trace window that is smaller than 1 millisecond
      const traceWindow = Trace.Helpers.Timing.traceWindowFromMicroSeconds(
          Trace.Types.Timing.Micro(1000),
          Trace.Types.Timing.Micro(1500),
      );
      const maxTraceWindow = Trace.Helpers.Timing.traceWindowFromMicroSeconds(
          milliToMicro(0),
          milliToMicro(100),
      );

      const expandedTraceWindow =
          Trace.Helpers.Timing.expandWindowByPercentOrToOneMillisecond(traceWindow, maxTraceWindow, 5);

      // Make sure the window was expanded to 1 millisecond instead of 5 percent.
      assert.strictEqual(expandedTraceWindow.range, 1000);
      // The middle of the window should not change
      assert.strictEqual(
          (traceWindow.max + traceWindow.min) / 2, (expandedTraceWindow.max + expandedTraceWindow.min) / 2);

      assert.strictEqual(expandedTraceWindow.min, 750);
      assert.strictEqual(expandedTraceWindow.max, 1750);
    });

    it('window does not expand past the provided max window', async function() {
      const traceWindow = Trace.Helpers.Timing.traceWindowFromMicroSeconds(
          milliToMicro(5),
          milliToMicro(55),
      );
      const maxTraceWindow = Trace.Helpers.Timing.traceWindowFromMicroSeconds(
          milliToMicro(0),
          milliToMicro(100),
      );

      const expandedTraceWindow =
          Trace.Helpers.Timing.expandWindowByPercentOrToOneMillisecond(traceWindow, maxTraceWindow, 50);
      assert.strictEqual(expandedTraceWindow.range, 67500);
      // Since the expanded window min bound would be smaller than the max window min bound, the expanded window min should be equal to the max window min
      assert.strictEqual(expandedTraceWindow.min, 0);
      assert.strictEqual(expandedTraceWindow.max, 67500);
    });
  });

  describe('BoundsIncludeTimeRange', () => {
    const {boundsIncludeTimeRange, traceWindowFromMicroSeconds} = Trace.Helpers.Timing;

    it('is false for an event that is outside the LHS of the visible bounds', () => {
      const bounds = traceWindowFromMicroSeconds(
          milliToMicro(50),
          milliToMicro(100),
      );

      const timeRange = traceWindowFromMicroSeconds(
          milliToMicro(10),
          milliToMicro(20),
      );

      assert.isFalse(boundsIncludeTimeRange({
        bounds,
        timeRange,
      }));
    });

    it('is false for an event that is outside the RHS of the visible bounds', () => {
      const bounds = traceWindowFromMicroSeconds(
          milliToMicro(50),
          milliToMicro(100),
      );

      const timeRange = traceWindowFromMicroSeconds(
          milliToMicro(101),
          milliToMicro(200),
      );

      assert.isFalse(boundsIncludeTimeRange({
        bounds,
        timeRange,
      }));
    });

    it('is true for an event that overlaps the LHS of the bounds', () => {
      const bounds = traceWindowFromMicroSeconds(
          milliToMicro(50),
          milliToMicro(100),
      );

      const timeRange = traceWindowFromMicroSeconds(
          milliToMicro(0),
          milliToMicro(52),
      );

      assert.isTrue(boundsIncludeTimeRange({
        bounds,
        timeRange,
      }));
    });

    it('is true for an event that overlaps the RHS of the bounds', () => {
      const bounds = traceWindowFromMicroSeconds(
          milliToMicro(50),
          milliToMicro(100),
      );

      const timeRange = traceWindowFromMicroSeconds(
          milliToMicro(99),
          milliToMicro(101),
      );

      assert.isTrue(boundsIncludeTimeRange({
        bounds,
        timeRange,
      }));
    });

    it('is true for an event that is entirely within the bounds', () => {
      const bounds = traceWindowFromMicroSeconds(
          milliToMicro(50),
          milliToMicro(100),
      );

      const timeRange = traceWindowFromMicroSeconds(
          milliToMicro(51),
          milliToMicro(75),
      );

      assert.isTrue(boundsIncludeTimeRange({
        bounds,
        timeRange,
      }));
    });

    it('is true for an event that is larger than the bounds', () => {
      const bounds = traceWindowFromMicroSeconds(
          milliToMicro(50),
          milliToMicro(100),
      );

      const timeRange = traceWindowFromMicroSeconds(
          milliToMicro(0),
          milliToMicro(200),
      );

      assert.isTrue(boundsIncludeTimeRange({
        bounds,
        timeRange,
      }));
    });
  });

  describe('timestampIsInBounds', () => {
    const {eventIsInBounds} = Trace.Helpers.Timing;
    const {Micro: MicroSeconds} = Trace.Types.Timing;

    const bounds: Trace.Types.Timing.TraceWindowMicro = {
      min: MicroSeconds(100),
      max: MicroSeconds(200),
      range: MicroSeconds(100),
    };

    const makeEvent = (ts: number, dur: number) => ({
                                                     ts: Trace.Types.Timing.Micro(ts),
                                                     dur: Trace.Types.Timing.Micro(dur),
                                                   }) as unknown as Trace.Types.Events.Event;

    // Left boundary
    assert.isTrue(eventIsInBounds(makeEvent(101, 1), bounds));
    assert.isTrue(eventIsInBounds(makeEvent(100, 1), bounds));
    assert.isTrue(eventIsInBounds(makeEvent(99, 1), bounds));
    assert.isTrue(eventIsInBounds(makeEvent(150, 500), bounds));
    assert.isFalse(eventIsInBounds(makeEvent(98, 1), bounds));
    assert.isFalse(eventIsInBounds(makeEvent(0, 1), bounds));
    assert.isFalse(eventIsInBounds(makeEvent(0, 0), bounds));

    // Right boundary
    assert.isTrue(eventIsInBounds(makeEvent(199, 1), bounds));
    assert.isTrue(eventIsInBounds(makeEvent(200, 1), bounds));
    assert.isFalse(eventIsInBounds(makeEvent(201, 1), bounds));
    assert.isFalse(eventIsInBounds(makeEvent(300, 50), bounds));
  });

  describe('timestampIsInBounds', () => {
    const {timestampIsInBounds} = Trace.Helpers.Timing;
    const {Micro: MicroSeconds} = Trace.Types.Timing;
    it('is true if the value is in the bounds and false otherwise', async () => {
      const bounds: Trace.Types.Timing.TraceWindowMicro = {
        min: MicroSeconds(1),
        max: MicroSeconds(10),
        range: MicroSeconds(9),
      };

      assert.isTrue(timestampIsInBounds(bounds, MicroSeconds(1)));
      assert.isTrue(timestampIsInBounds(bounds, MicroSeconds(5)));
      assert.isTrue(timestampIsInBounds(bounds, MicroSeconds(10)));
      assert.isFalse(timestampIsInBounds(bounds, MicroSeconds(0)));
      assert.isFalse(timestampIsInBounds(bounds, MicroSeconds(11)));
    });
  });

  describe('WindowFitsInsideBounds', () => {
    const {windowFitsInsideBounds} = Trace.Helpers.Timing;
    const {Micro: MicroSeconds} = Trace.Types.Timing;

    const bounds: Trace.Types.Timing.TraceWindowMicro = {
      min: MicroSeconds(5),
      max: MicroSeconds(15),
      range: MicroSeconds(10),
    };

    it('is true if the window fits within the bounds', () => {
      assert.isTrue(windowFitsInsideBounds({window: microsecondsTraceWindow(5, 8), bounds}));
      assert.isTrue(windowFitsInsideBounds({window: microsecondsTraceWindow(5, 14), bounds}));
      assert.isTrue(windowFitsInsideBounds({window: microsecondsTraceWindow(5, 15), bounds}));
    });

    it('is false if the window does not fully fit within the bounds', () => {
      // Outside the left hand edge
      assert.isFalse(windowFitsInsideBounds({window: microsecondsTraceWindow(0, 8), bounds}));
      // Outside the right hand edge
      assert.isFalse(windowFitsInsideBounds({window: microsecondsTraceWindow(10, 20), bounds}));
      // Outside entirely before
      assert.isFalse(windowFitsInsideBounds({window: microsecondsTraceWindow(0, 5), bounds}));
      // Outside entirely after
      assert.isFalse(windowFitsInsideBounds({window: microsecondsTraceWindow(20, 25), bounds}));
    });
  });
});
