// Copyright 2023 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 * as Trace from '../../../models/trace/trace.js';
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {setupIgnoreListManagerEnvironment} from '../../../testing/TraceHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as PerfUI from '../../../ui/legacy/components/perf_ui/perf_ui.js';
import * as Timeline from '../timeline.js';

describeWithEnvironment('CompatibilityTracksAppender', function() {
  let parsedTrace: Trace.Handlers.Types.ParsedTrace;
  let tracksAppender: Timeline.CompatibilityTracksAppender.CompatibilityTracksAppender;
  let entryData: Trace.Types.Events.Event[] = [];
  let flameChartData = PerfUI.FlameChart.FlameChartTimelineData.createEmpty();
  let entryTypeByLevel: Timeline.TimelineFlameChartDataProvider.EntryType[] = [];

  async function initTrackAppender(
      context: Mocha.Suite|Mocha.Context, fixture = 'timings-track.json.gz'): Promise<void> {
    entryData = [];
    flameChartData = PerfUI.FlameChart.FlameChartTimelineData.createEmpty();
    entryTypeByLevel = [];
    ({parsedTrace} = await TraceLoader.traceEngine(context, fixture));
    tracksAppender = new Timeline.CompatibilityTracksAppender.CompatibilityTracksAppender(
        flameChartData, parsedTrace, entryData, entryTypeByLevel);
    const timingsTrack = tracksAppender.timingsTrackAppender();
    const gpuTrack = tracksAppender.gpuTrackAppender();
    const threadAppenders = tracksAppender.threadAppenders();
    let currentLevel = timingsTrack.appendTrackAtLevel(0);
    currentLevel = gpuTrack.appendTrackAtLevel(currentLevel);
    for (const threadAppender of threadAppenders) {
      currentLevel = threadAppender.appendTrackAtLevel(currentLevel);
    }
  }

  beforeEach(async () => {
    setupIgnoreListManagerEnvironment();
    await initTrackAppender(this);
  });

  describe('Tree view data', () => {
    describe('eventsInTrack', () => {
      it('returns all the events appended by a track with multiple levels', () => {
        const timingsTrack = tracksAppender.timingsTrackAppender();
        const timingsTrackEvents = tracksAppender.eventsInTrack(timingsTrack);
        const allTimingEvents = [
          ...parsedTrace.UserTimings.consoleTimings,
          ...parsedTrace.UserTimings.timestampEvents,
          ...parsedTrace.UserTimings.performanceMarks,
          ...parsedTrace.UserTimings.performanceMeasures,
        ].sort((a, b) => a.ts - b.ts);
        assert.deepEqual(timingsTrackEvents, allTimingEvents);
      });

      it('returns all the events appended by a track with one level', () => {
        const gpuTrack = tracksAppender.gpuTrackAppender();
        const gpuTrackEvents = tracksAppender.eventsInTrack(gpuTrack) as readonly Trace.Types.Events.Event[];
        assert.deepEqual(gpuTrackEvents, parsedTrace.GPU.mainGPUThreadTasks);
      });
    });
    describe('eventsForTreeView', () => {
      it('returns only sync events if using async events means a tree cannot be built', () => {
        const timingsTrack = tracksAppender.timingsTrackAppender();
        const timingsEvents = tracksAppender.eventsInTrack(timingsTrack);
        assert.isFalse(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(timingsEvents));
        const treeEvents = tracksAppender.eventsForTreeView(timingsTrack);
        const allEventsAreSync = treeEvents.every(event => !Trace.Types.Events.isPhaseAsync(event.ph));
        assert.isTrue(allEventsAreSync);
      });
      it('returns both sync and async events if a tree can be built with them', async () => {
        // This file contains events in the timings track that can be assembled as a tree
        await initTrackAppender(this, 'sync-like-timings.json.gz');
        const timingsTrack = tracksAppender.timingsTrackAppender();
        const timingsEvents = tracksAppender.eventsInTrack(timingsTrack);
        assert.isTrue(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(timingsEvents));
        const treeEvents = tracksAppender.eventsForTreeView(timingsTrack);
        assert.deepEqual(treeEvents, timingsEvents);
      });

      it('returns events for tree view for nested tracks', async () => {
        // This file contains two rasterizer threads which should be
        // nested inside the same header.
        await initTrackAppender(this, 'lcp-images-rasterizer.json.gz');
        const rasterTracks = tracksAppender.threadAppenders().filter(
            threadAppender => threadAppender.threadType === Trace.Handlers.Threads.ThreadType.RASTERIZER);
        assert.lengthOf(rasterTracks, 2);

        const raster1Events = tracksAppender.eventsInTrack(rasterTracks[0]);
        assert.lengthOf(raster1Events, 6);
        assert.isTrue(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(raster1Events));
        const raster1TreeEvents = tracksAppender.eventsForTreeView(rasterTracks[0]);
        assert.deepEqual(raster1TreeEvents, raster1Events);

        const raster2Events = tracksAppender.eventsInTrack(rasterTracks[1]);
        assert.lengthOf(raster2Events, 1);
        assert.isTrue(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(raster2Events));
        const raster2TreeEvents = tracksAppender.eventsForTreeView(rasterTracks[1]);
        assert.deepEqual(raster2TreeEvents, raster2Events);
      });
    });
    describe('groupEventsForTreeView', () => {
      it('returns all the events of a flame chart group with multiple levels', async () => {
        // This file contains events in the timings track that can be assembled as a tree
        await initTrackAppender(this, 'sync-like-timings.json.gz');
        const timingsGroupEvents = tracksAppender.groupEventsForTreeView(flameChartData.groups[0]);
        if (!timingsGroupEvents) {
          assert.fail('Could not find events for group');
        }
        const allTimingEvents = [
          ...parsedTrace.UserTimings.consoleTimings,
          ...parsedTrace.UserTimings.timestampEvents,
          ...parsedTrace.UserTimings.performanceMarks,
          ...parsedTrace.UserTimings.performanceMeasures,
        ].sort((a, b) => a.ts - b.ts);
        assert.deepEqual(timingsGroupEvents, allTimingEvents);
      });
      it('returns all the events of a flame chart group with one level', () => {
        const gpuGroupEvents =
            tracksAppender.groupEventsForTreeView(flameChartData.groups[1]) as readonly Trace.Types.Events.Event[];
        if (!gpuGroupEvents) {
          assert.fail('Could not find events for group');
        }
        assert.deepEqual(gpuGroupEvents, parsedTrace.GPU.mainGPUThreadTasks);
      });
    });
  });

  describe('popoverInfo', () => {
    it('shows the correct warning for a long task when hovered', async function() {
      await initTrackAppender(this, 'simple-js-program.json.gz');
      const events = parsedTrace.Renderer?.allTraceEntries;
      if (!events) {
        throw new Error('Could not find renderer events');
      }
      const longTask = events.find(e => (e.dur || 0) > 1_000_000);
      if (!longTask) {
        throw new Error('Could not find long task');
      }
      const info = tracksAppender.popoverInfo(longTask, 2);
      assert.strictEqual(info.warningElements?.length, 1);
      const warning = info.warningElements?.[0];
      if (!(warning instanceof HTMLSpanElement)) {
        throw new Error('Found unexpected warning');
      }
      assert.strictEqual(warning?.innerText, 'Long task took 1.30\u00A0s.');
    });

    it('shows the correct warning for a forced recalc styles when hovered', async function() {
      await initTrackAppender(this, 'large-layout-small-recalc.json.gz');
      const events = parsedTrace.Warnings.perWarning.get('FORCED_REFLOW') || [];

      if (!events) {
        throw new Error('Could not find forced reflows events');
      }
      const recalcStyles = events[0];
      if (!recalcStyles) {
        throw new Error('Could not find recalc styles');
      }
      const info = tracksAppender.popoverInfo(recalcStyles, 2);
      assert.strictEqual(info.warningElements?.length, 1);
      const warning = info.warningElements?.[0];
      if (!(warning instanceof HTMLSpanElement)) {
        throw new Error('Found unexpected warning');
      }
      assert.strictEqual(warning?.innerText, 'Forced reflow is a likely performance bottleneck.');
    });

    it('shows the correct warning for a forced layout when hovered', async function() {
      await initTrackAppender(this, 'large-layout-small-recalc.json.gz');
      const events = parsedTrace.Warnings.perWarning.get('FORCED_REFLOW') || [];

      if (!events) {
        throw new Error('Could not find forced reflows events');
      }
      const layout = events[1];
      if (!layout) {
        throw new Error('Could not find layout');
      }
      const info = tracksAppender.popoverInfo(layout, 2);
      assert.strictEqual(info.warningElements?.length, 1);
      const warning = info.warningElements?.[0];
      if (!(warning instanceof HTMLSpanElement)) {
        throw new Error('Found unexpected warning');
      }
      assert.strictEqual(warning?.innerText, 'Forced reflow is a likely performance bottleneck.');
    });

    it('shows the correct warning for slow idle callbacks', async function() {
      await initTrackAppender(this, 'idle-callback.json.gz');
      const events = parsedTrace.Renderer?.allTraceEntries;
      if (!events) {
        throw new Error('Could not find renderer events');
      }
      const idleCallback = events.find(event => {
        const {duration} = Trace.Helpers.Timing.eventTimingsMilliSeconds(event);
        if (!Trace.Types.Events.isFireIdleCallback(event)) {
          return false;
        }
        if (duration <= event.args.data.allottedMilliseconds) {
          false;
        }
        return true;
      });
      if (!idleCallback) {
        throw new Error('Could not find idle callback');
      }
      const info = tracksAppender.popoverInfo(idleCallback, 2);
      assert.strictEqual(info.warningElements?.length, 1);
      const warning = info.warningElements?.[0];
      if (!(warning instanceof HTMLSpanElement)) {
        throw new Error('Found unexpected warning');
      }
      assert.strictEqual(warning?.innerText, 'Idle callback execution extended beyond deadline by 79.56\u00A0ms');
    });
  });

  it('can return the group for a given level', async () => {
    await initTrackAppender(this, 'web-dev-with-commit.json.gz');
    // The order of these groups might seem odd, but it's based on the setup in
    // the initTrackAppender function which does GPU and then threads.
    const groupForLevel0 = tracksAppender.groupForLevel(0);
    assert.strictEqual(groupForLevel0?.name, 'GPU');
    const groupForLevel1 = tracksAppender.groupForLevel(1);
    assert.strictEqual(groupForLevel1?.name, 'Main — https://web.dev/');
  });
});
