// Copyright 2024 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 {dispatchClickEvent} from '../../../testing/DOMHelpers.js';
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {
  makeInstantEvent,
  microsecondsTraceWindow,
  MockFlameChartDelegate,
  setupIgnoreListManagerEnvironment,
} from '../../../testing/TraceHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as PerfUI from '../../../ui/legacy/components/perf_ui/perf_ui.js';
import * as Timeline from '../timeline.js';

import * as Components from './components/components.js';
import * as Overlays from './overlays.js';

const FAKE_OVERLAY_ENTRY_QUERIES: Overlays.Overlays.OverlayEntryQueries = {
  isEntryCollapsedByUser() {
    return false;
  },
  firstVisibleParentForEntry() {
    return null;
  },
};

/**
 * The Overlays expects to be provided with both the main and network charts
 * and data providers. This function creates all of those and optionally sets
 * the trace data for the providers if it is provided.
 */
function createCharts(parsedTrace?: Trace.Handlers.Types.ParsedTrace): Overlays.Overlays.TimelineCharts {
  const mainProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
  const networkProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
  if (parsedTrace) {
    mainProvider.setModel(parsedTrace);
    networkProvider.setModel(parsedTrace);
  }

  const delegate = new MockFlameChartDelegate();
  const mainChart = new PerfUI.FlameChart.FlameChart(mainProvider, delegate);
  const networkChart = new PerfUI.FlameChart.FlameChart(networkProvider, delegate);
  // Add to DOM for offsetWidth, etc working
  document.body.append(mainChart.element, networkChart.element);

  if (parsedTrace) {
    // Force the charts to render. Normally the TimelineFlameChartView would do
    // this, but we aren't creating one for these tests.
    mainChart.update();
    networkChart.update();
  }

  return {
    mainProvider,
    mainChart,
    networkProvider,
    networkChart,
  };
}

describeWithEnvironment('Overlays', () => {
  beforeEach(() => {
    setupIgnoreListManagerEnvironment();
  });

  afterEach(() => {
    // Remove any FlameChart elements from the DOM
    document.body.querySelectorAll('widget').forEach(e => e.remove());
  });

  it('can calculate the x position of an event based on the dimensions and its timestamp', async () => {
    const flameChartsContainer = document.createElement('div');
    const mainFlameChartsContainer = flameChartsContainer.createChild('div');
    const networkFlameChartsContainer = flameChartsContainer.createChild('div');
    const container = flameChartsContainer.createChild('div');

    const overlays = new Overlays.Overlays.Overlays({
      container,
      flameChartsContainers: {
        main: mainFlameChartsContainer,
        network: networkFlameChartsContainer,
      },
      charts: createCharts(),
      entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
    });

    // Set up the dimensions so it is 100px wide
    overlays.updateChartDimensions('main', {
      widthPixels: 100,
      heightPixels: 50,
      scrollOffsetPixels: 0,
      allGroupsCollapsed: false,
    });
    overlays.updateChartDimensions('network', {
      widthPixels: 100,
      heightPixels: 50,
      scrollOffsetPixels: 0,
      allGroupsCollapsed: false,
    });

    const windowMin = Trace.Types.Timing.Micro(0);
    const windowMax = Trace.Types.Timing.Micro(100);
    // Set the visible window to be 0-100 microseconds
    overlays.updateVisibleWindow(Trace.Helpers.Timing.traceWindowFromMicroSeconds(windowMin, windowMax));

    // Now set an event to be at 50 microseconds.
    const event = makeInstantEvent('test-event', 50);

    const xPosition = overlays.xPixelForEventStartOnChart(event);
    assert.strictEqual(xPosition, 50);
  });

  it('can calculate the y position of a main chart event', async function() {
    const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
    const charts = createCharts(parsedTrace);

    const flameChartsContainer = document.createElement('div');
    const mainFlameChartsContainer = flameChartsContainer.createChild('div');
    const networkFlameChartsContainer = flameChartsContainer.createChild('div');
    const container = flameChartsContainer.createChild('div');

    const overlays = new Overlays.Overlays.Overlays({
      container,
      flameChartsContainers: {
        main: mainFlameChartsContainer,
        network: networkFlameChartsContainer,
      },
      charts,
      entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
    });

    overlays.updateChartDimensions('main', {
      widthPixels: 1000,
      heightPixels: 500,
      scrollOffsetPixels: 0,
      allGroupsCollapsed: false,
    });
    overlays.updateChartDimensions('network', {
      widthPixels: 1000,
      heightPixels: 200,
      scrollOffsetPixels: 0,
      allGroupsCollapsed: false,
    });

    // Set the visible window to be the entire trace.
    overlays.updateVisibleWindow(parsedTrace.Meta.traceBounds);

    const event = charts.mainProvider.eventByIndex?.(50);
    assert.isOk(event);
    const yPixel = overlays.yPixelForEventOnChart(event);
    // The Y offset for the main chart is 280px, but we add 208px on (200px for the
    // network chart, and 8px for the re-size handle) giving us the expected
    // 441px.
    assert.strictEqual(yPixel, 488);
  });

  it('can adjust the y position of a main chart event when the network track is collapsed', async function() {
    const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
    const charts = createCharts(parsedTrace);

    const flameChartsContainer = document.createElement('div');
    const mainFlameChartsContainer = flameChartsContainer.createChild('div');
    const networkFlameChartsContainer = flameChartsContainer.createChild('div');
    const container = flameChartsContainer.createChild('div');

    const overlays = new Overlays.Overlays.Overlays({
      container,
      flameChartsContainers: {
        main: mainFlameChartsContainer,
        network: networkFlameChartsContainer,
      },
      charts,
      entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
    });

    overlays.updateChartDimensions('main', {
      widthPixels: 1000,
      heightPixels: 500,
      scrollOffsetPixels: 0,
      allGroupsCollapsed: false,
    });
    overlays.updateChartDimensions('network', {
      widthPixels: 1000,
      heightPixels: 34,
      scrollOffsetPixels: 0,
      // Make the network track collapsed
      allGroupsCollapsed: true,
    });

    // Set the visible window to be the entire trace.
    overlays.updateVisibleWindow(parsedTrace.Meta.traceBounds);

    const event = charts.mainProvider.eventByIndex?.(50);
    assert.isOk(event);
    const yPixel = overlays.yPixelForEventOnChart(event);
    // The Y offset for the main chart is 280px, but we add 34px on (the height
    // of the collapsed network chart, with no resizer bar as it is hidden when
    // the network track is collapsed). This gives us 280+34 = 314.
    assert.strictEqual(yPixel, 314);
  });

  it('can calculate the y position of a network chart event', async function() {
    const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
    const charts = createCharts(parsedTrace);

    const flameChartsContainer = document.createElement('div');
    const mainFlameChartsContainer = flameChartsContainer.createChild('div');
    const networkFlameChartsContainer = flameChartsContainer.createChild('div');
    const container = flameChartsContainer.createChild('div');

    const overlays = new Overlays.Overlays.Overlays({
      container,
      flameChartsContainers: {
        main: mainFlameChartsContainer,
        network: networkFlameChartsContainer,
      },
      charts,
      entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
    });

    overlays.updateChartDimensions('main', {
      widthPixels: 1000,
      heightPixels: 500,
      scrollOffsetPixels: 0,
      allGroupsCollapsed: false,
    });
    overlays.updateChartDimensions('network', {
      widthPixels: 1000,
      heightPixels: 200,
      scrollOffsetPixels: 0,
      allGroupsCollapsed: false,
    });

    // Set the visible window to be the entire trace.
    overlays.updateVisibleWindow(parsedTrace.Meta.traceBounds);

    // Fake the level being visible: because we don't fully render the chart we
    // need to fake this for this test.
    sinon.stub(charts.networkChart, 'levelIsVisible').callsFake(() => true);

    // Find an event on the network chart
    const event = charts.networkProvider.eventByIndex?.(0);
    assert.isOk(event);
    const yPixel = overlays.yPixelForEventOnChart(event);
    // This event is in the first level, but the first level has some offset
    // above it to allow for the header row and the row with the timestamps on
    // it, hence why this value is not 0px.
    assert.strictEqual(yPixel, 34);
  });

  describe('rendering overlays', () => {
    function setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace: Trace.Handlers.Types.ParsedTrace): {
      container: HTMLElement,
      overlays: Overlays.Overlays.Overlays,
      charts: Overlays.Overlays.TimelineCharts,
    } {
      const charts = createCharts(parsedTrace);

      const flameChartsContainer = document.createElement('div');
      const mainFlameChartsContainer = flameChartsContainer.createChild('div');
      const networkFlameChartsContainer = flameChartsContainer.createChild('div');
      const container = flameChartsContainer.createChild('div');

      const overlays = new Overlays.Overlays.Overlays({
        container,
        flameChartsContainers: {
          main: mainFlameChartsContainer,
          network: networkFlameChartsContainer,
        },
        charts,
        entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
      });
      const currManager = Timeline.ModificationsManager.ModificationsManager.activeManager();
      // The Annotations Overlays are added through the ModificationsManager listener
      currManager?.addEventListener(Timeline.ModificationsManager.AnnotationModifiedEvent.eventName, async event => {
        const {overlay, action} = (event as Timeline.ModificationsManager.AnnotationModifiedEvent);
        if (action === 'Add') {
          overlays.add(overlay);
        }
        await overlays.update();
      });

      // When an annotation overlay is remomved, this event is dispatched to the Modifications Manager.
      overlays.addEventListener(Overlays.Overlays.AnnotationOverlayActionEvent.eventName, async event => {
        const {overlay, action} = (event as Overlays.Overlays.AnnotationOverlayActionEvent);
        if (action === 'Remove') {
          overlays.remove(overlay);
        }
        await overlays.update();
      });

      overlays.updateChartDimensions('main', {
        widthPixels: 1000,
        heightPixels: 500,
        scrollOffsetPixels: 0,
        allGroupsCollapsed: false,
      });
      overlays.updateChartDimensions('network', {
        widthPixels: 1000,
        heightPixels: 200,
        scrollOffsetPixels: 0,
        allGroupsCollapsed: false,
      });

      // Set the visible window to be the entire trace.
      overlays.updateVisibleWindow(parsedTrace.Meta.traceBounds);
      return {overlays, container, charts};
    }

    it('can render an entry selected overlay', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      overlays.add({
        type: 'ENTRY_SELECTED',
        entry: event,
      });
      await overlays.update();

      // Ensure that the overlay was created.
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_SELECTED');
      assert.isOk(overlayDOM);
    });

    it('renders an ENTRY_OUTLINE even if the entry is also the ENTRY_SELECTED entry', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);
      overlays.add({
        type: 'ENTRY_OUTLINE',
        entry: event,
        outlineReason: 'ERROR',
      });
      await overlays.update();

      const outlineVisible =
          container.querySelector<HTMLElement>('.overlay-type-ENTRY_OUTLINE')?.style.display === 'block';
      assert.isTrue(outlineVisible, 'The ENTRY_OUTLINE should be visible');

      // Now make a selected entry too
      overlays.add({
        type: 'ENTRY_SELECTED',
        entry: event,
      });
      await overlays.update();
      const outlineStillVisible =
          container.querySelector<HTMLElement>('.overlay-type-ENTRY_OUTLINE')?.style.display === 'block';
      assert.isTrue(outlineStillVisible, 'The ENTRY_OUTLINE should be visible');
    });

    it('only ever renders a single selected overlay', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event1 = charts.mainProvider.eventByIndex?.(50);
      const event2 = charts.mainProvider.eventByIndex?.(51);
      assert.isOk(event1);
      assert.isOk(event2);

      overlays.add({
        type: 'ENTRY_SELECTED',
        entry: event1,
      });
      await overlays.update();
      overlays.add({
        type: 'ENTRY_SELECTED',
        entry: event2,
      });
      await overlays.update();

      // There should only be one of these
      const entrySelectedOverlays = container.querySelectorAll<HTMLElement>('.overlay-type-ENTRY_SELECTED');
      assert.lengthOf(entrySelectedOverlays, 1);
    });

    it('can render entry label overlay', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      overlays.add({
        type: 'ENTRY_LABEL',
        entry: event,
        label: 'entry label',
      });
      await overlays.update();

      // Ensure that the overlay was created.
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
      assert.isOk(overlayDOM);
    });

    it('dispatches an event when the entry label overlay is clicked', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      overlays.add({
        type: 'ENTRY_LABEL',
        entry: event,
        label: 'entry label',
      });
      await overlays.update();

      // Ensure that the overlay was created.
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
      assert.isOk(overlayDOM);

      const overlayClick = new Promise<Overlays.Overlays.EntryLabel>(resolve => {
        overlays.addEventListener(Overlays.Overlays.EntryLabelMouseClick.eventName, e => {
          const event = e as Overlays.Overlays.EntryLabelMouseClick;
          resolve(event.overlay);
        }, {once: true});
      });

      dispatchClickEvent(overlayDOM);
      const overlayFromEvent = await overlayClick;
      // Check that the event was dispatched on the right overlay.
      assert.deepEqual(overlayFromEvent, {
        type: 'ENTRY_LABEL',
        entry: event,
        label: 'entry label',
      });
    });

    it('toggles overlays container display', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);

      overlays.toggleAllOverlaysDisplayed(true);
      await overlays.update();

      assert.strictEqual(container.style.display, 'block');

      overlays.toggleAllOverlaysDisplayed(false);
      await overlays.update();

      assert.strictEqual(container.style.display, 'none');

      overlays.toggleAllOverlaysDisplayed(true);
      await overlays.update();

      assert.strictEqual(container.style.display, 'block');
    });

    it('only renders one TIMESTAMP_MARKER as it is a singleton', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      overlays.add({
        type: 'TIMESTAMP_MARKER',
        timestamp: parsedTrace.Meta.traceBounds.min,
      });
      overlays.add({
        type: 'TIMESTAMP_MARKER',
        timestamp: parsedTrace.Meta.traceBounds.max,
      });
      await overlays.update();
      assert.lengthOf(container.children, 1);
    });

    it('can render the label for entry label overlay', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      overlays.add({
        type: 'ENTRY_LABEL',
        entry: event,
        label: 'entry label',
      });
      await overlays.update();

      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
      assert.isOk(overlayDOM);
      const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
      assert.isOk(component?.shadowRoot);

      const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
      assert.isOk(elementsWrapper);

      const inputField = elementsWrapper.querySelector<HTMLElement>('.input-field');
      assert.isOk(inputField);

      assert.strictEqual(inputField?.innerText, 'entry label');
    });

    it('Inputting `Enter`into label overlay makes it non-editable', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      // Create an entry label overlay
      overlays.add({
        type: 'ENTRY_LABEL',
        entry: event,
        label: 'label',
      });
      await overlays.update();

      // Ensure that the overlay was created.
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
      assert.isOk(overlayDOM);

      const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
      assert.isOk(component?.shadowRoot);
      component.connectedCallback();
      const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
      assert.isOk(elementsWrapper);

      const inputField = elementsWrapper.querySelector<HTMLElement>('.input-field');
      assert.isOk(inputField);

      // Double click on the label box to make it editable and focus on it
      inputField.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));

      // Ensure the label content is editable
      assert.isTrue(inputField.isContentEditable);

      // Press `Enter` to make the label not editable
      inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', cancelable: true, bubbles: true}));

      // Ensure the label content is not editable
      assert.isFalse(inputField.isContentEditable);
    });

    it('Inputting `Enter` into time range label field when the label is empty removes the overlay', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      // Create a time range overlay with an empty label
      overlays.add({
        type: 'TIME_RANGE',
        label: '',
        showDuration: true,
        // Make this overlay the entire span of the trace
        bounds: parsedTrace.Meta.traceBounds,
      });
      await overlays.update();

      // Ensure that the overlay was created.
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
      assert.isOk(overlayDOM);

      const component = overlayDOM?.querySelector('devtools-time-range-overlay');
      assert.isOk(component?.shadowRoot);
      component.connectedCallback();
      const rangeContainer = component.shadowRoot.querySelector<HTMLElement>('.range-container');
      assert.isOk(rangeContainer);

      const labelBox = rangeContainer.querySelector<HTMLElement>('.label-text');
      assert.isOk(labelBox);

      // Double click on the label box to make it editable and focus on it
      labelBox.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));

      // Press `Enter` on the label field
      labelBox.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', cancelable: true, bubbles: true}));

      // Ensure that the entry overlay has been removed because it was saved empty
      assert.lengthOf(overlays.overlaysOfType('TIME_RANGE'), 0);
    });

    it('Inputting `Enter` into time range label field when the label is not empty does not remove the overlay',
       async function() {
         const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
         const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
         const event = charts.mainProvider.eventByIndex?.(50);
         assert.isOk(event);

         // Create a time range overlay with a label
         overlays.add({
           type: 'TIME_RANGE',
           label: 'label',
           showDuration: true,
           // Make this overlay the entire span of the trace
           bounds: parsedTrace.Meta.traceBounds,
         });
         await overlays.update();

         // Ensure that the overlay was created.
         const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
         assert.isOk(overlayDOM);

         const component = overlayDOM?.querySelector('devtools-time-range-overlay');
         assert.isOk(component?.shadowRoot);
         component.connectedCallback();
         const rangeContainer = component.shadowRoot.querySelector<HTMLElement>('.range-container');
         assert.isOk(rangeContainer);

         const labelBox = rangeContainer.querySelector<HTMLElement>('.label-text');
         assert.isOk(labelBox);

         // Double click on the label box to make it editable and focus on it
         labelBox.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));

         // Press `Enter` on the label field
         labelBox.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', cancelable: true, bubbles: true}));

         // Ensure that the entry overlay has not been because it was has a non-empty label
         assert.lengthOf(overlays.overlaysOfType('TIME_RANGE'), 1);
       });

    it('Can create multiple Time Range Overlays for Time Range annotations', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
        type: 'TIME_RANGE',
        label: 'label',
        bounds: parsedTrace.Meta.traceBounds,
      });

      Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
        type: 'TIME_RANGE',
        label: 'label2',
        bounds: parsedTrace.Meta.traceBounds,
      });
      await overlays.update();

      assert.lengthOf(overlays.overlaysOfType('TIME_RANGE'), 2);
    });

    it('Removes empty label if it is empty when navigated away from (removed focused from)', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      // Create an entry label overlay
      Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
        type: 'ENTRY_LABEL',
        entry: event as Trace.Types.Events.Event,
        label: '',
      });
      await overlays.update();

      // Ensure that the overlay was created.
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
      assert.isOk(overlayDOM);
      const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
      assert.isOk(component?.shadowRoot);

      component.connectedCallback();
      const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
      assert.isOk(elementsWrapper);

      const inputField = elementsWrapper.querySelector<HTMLElement>('.input-field');
      assert.isOk(inputField);

      // Double click on the label box to make it editable and focus on it
      inputField.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));

      // Ensure that the entry has 1 overlay
      assert.lengthOf(overlays.overlaysForEntry(event), 1);

      // Change the content to not editable by changing the element blur like when clicking outside of it.
      // The label is empty since no initial value was passed into it and no characters were entered.
      inputField.dispatchEvent(new FocusEvent('blur', {bubbles: true}));

      // Ensure that the entry overlay has been removed because it was saved empty
      assert.lengthOf(overlays.overlaysForEntry(event), 0);
    });

    it('Update label overlay when the label changes', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      // Create an entry label overlay
      Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
        type: 'ENTRY_LABEL',
        entry: event as Trace.Types.Events.Event,
        label: '',
      });
      await overlays.update();

      // Ensure that the overlay was created.
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
      assert.isOk(overlayDOM);
      const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
      assert.isOk(component?.shadowRoot);

      component.connectedCallback();
      component.dispatchEvent(new Components.EntryLabelOverlay.EntryLabelChangeEvent('new label'));

      const updatedOverlay = overlays.overlaysForEntry(event)[0] as Overlays.Overlays.EntryLabel;
      assert.isOk(updatedOverlay);
      // Make sure the label was updated in the Overlay Object
      assert.strictEqual(updatedOverlay.label, 'new label');
    });

    it('creates an overlay for a time range when an time range annotation is created', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);

      // Since TIME_RANGE is AnnotationOverlay, create it through ModificationsManager
      Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
        type: 'TIME_RANGE',
        label: '',
        // Make this overlay the entire span of the trace
        bounds: parsedTrace.Meta.traceBounds,
      });
      await overlays.update();
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
      assert.isOk(overlayDOM);
    });

    it('can render an overlay for a time range', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      overlays.add({
        type: 'TIME_RANGE',
        label: '',
        showDuration: true,
        // Make this overlay the entire span of the trace
        bounds: parsedTrace.Meta.traceBounds,
      });
      await overlays.update();
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
      assert.isOk(overlayDOM);
    });

    it('can update a time range overlay with new bounds', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const rangeOverlay = overlays.add({
        type: 'TIME_RANGE',
        label: '',
        showDuration: true,
        // Make this overlay the entire span of the trace
        bounds: parsedTrace.Meta.traceBounds,
      });
      await overlays.update();
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
      assert.isOk(overlayDOM);
      const firstWidth = window.parseInt(overlayDOM.style.width);

      // change the bounds so the new min is +1second of time.
      const newBounds = Trace.Helpers.Timing.traceWindowFromMicroSeconds(
          Trace.Types.Timing.Micro(rangeOverlay.bounds.min + (1_000 * 1_000)),
          rangeOverlay.bounds.max,
      );
      overlays.updateExisting(rangeOverlay, {bounds: newBounds});
      await overlays.update();
      const secondWidth = window.parseInt(overlayDOM.style.width);
      // The new time range is smaller so the DOM element should have less width
      assert.isTrue(secondWidth < firstWidth);
    });

    it('renders the overlay for a selected layout shift entry correctly', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'cls-single-frame.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const layoutShiftEvent = parsedTrace.LayoutShifts.clusters.at(0)?.events.at(0);
      if (!layoutShiftEvent) {
        throw new Error('layoutShiftEvent was unexpectedly undefined');
      }
      overlays.add({
        type: 'ENTRY_SELECTED',
        entry: layoutShiftEvent,
      });
      const boundsRange = Trace.Types.Timing.Micro(20_000);
      const boundsMax = Trace.Types.Timing.Micro(layoutShiftEvent.ts + boundsRange);
      overlays.updateVisibleWindow({min: layoutShiftEvent.ts, max: boundsMax, range: boundsRange});
      await overlays.update();
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_SELECTED');
      assert.isOk(overlayDOM);
      assert.strictEqual(window.parseInt(overlayDOM.style.width), 17);
    });

    it('renders the duration and label for a time range overlay', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      overlays.add({
        type: 'TIME_RANGE',
        label: '',
        showDuration: true,
        // Make this overlay the entire span of the trace
        bounds: parsedTrace.Meta.traceBounds,
      });
      await overlays.update();
      await RenderCoordinator.done();
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
      const component = overlayDOM?.querySelector('devtools-time-range-overlay');
      assert.isOk(component?.shadowRoot);
      const rangeContainer = component.shadowRoot.querySelector<HTMLElement>('.range-container');
      assert.isOk(rangeContainer);
      const duration = rangeContainer.querySelector<HTMLElement>('.duration');
      assert.isOk(duration);
      assert.strictEqual(duration?.innerText, '1.26\xA0s');
    });

    it('can remove an overlay', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      const selectedOverlay = overlays.add({
        type: 'ENTRY_SELECTED',
        entry: event,
      });
      await overlays.update();
      assert.lengthOf(container.children, 1);

      overlays.remove(selectedOverlay);
      await overlays.update();
      assert.lengthOf(container.children, 0);
    });

    it('can render an entry selected overlay for a frame', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
      const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const timelineFrame = charts.mainProvider.eventByIndex?.(5);
      assert.isOk(timelineFrame);

      overlays.add({
        type: 'ENTRY_SELECTED',
        entry: timelineFrame,
      });
      await overlays.update();

      // Ensure that the overlay was created.
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_SELECTED');
      assert.isOk(overlayDOM);
    });

    it('can return a list of overlays for an entry', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      overlays.add({
        type: 'ENTRY_SELECTED',
        entry: event,
      });

      const existingOverlays = overlays.overlaysForEntry(event);
      assert.deepEqual(existingOverlays, [{
                         type: 'ENTRY_SELECTED',
                         entry: event,
                       }]);
    });

    it('can delete overlays and remove them from the DOM', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {container, overlays, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      overlays.add({
        type: 'ENTRY_SELECTED',
        entry: event,
      });
      await overlays.update();

      assert.lengthOf(container.children, 1);
      const removedCount = overlays.removeOverlaysOfType('ENTRY_SELECTED');
      assert.strictEqual(removedCount, 1);
      assert.lengthOf(container.children, 0);
    });

    it('the label entry field is editable when created', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const charts = createCharts(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      // Since ENTRY_LABEL is AnnotationOverlay, create it through ModificationsManager
      Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
        type: 'ENTRY_LABEL',
        label: '',
        entry: event as Trace.Types.Events.Event,
      });

      await overlays.update();
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
      assert.isOk(overlayDOM);
      const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
      assert.isOk(component?.shadowRoot);

      const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
      const inputField = elementsWrapper?.querySelector<HTMLElement>('.input-field') as HTMLSpanElement;
      assert.isOk(inputField);
      // The label input box should be editable after it is created and before anything else happened
      assert.isTrue(inputField.isContentEditable);
    });

    it('the label entry field is in focus after being double clicked on', async function() {
      const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
      const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
      const charts = createCharts(parsedTrace);
      const event = charts.mainProvider.eventByIndex?.(50);
      assert.isOk(event);

      // Since ENTRY_LABEL is AnnotationOverlay, create it through ModificationsManager
      Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
        type: 'ENTRY_LABEL',
        label: '',
        entry: event as Trace.Types.Events.Event,
      });

      await overlays.update();
      const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
      assert.isOk(overlayDOM);
      const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
      assert.isOk(component?.shadowRoot);

      const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
      assert.isOk(elementsWrapper);
      const labelBox = elementsWrapper.querySelector<HTMLElement>('.input-field') as HTMLSpanElement;

      assert.isOk(labelBox);

      // The label input box should be editable after it is created and before anything else happened
      assert.isTrue(labelBox.isContentEditable);

      // Make the content to editable by changing the element blur like when clicking outside of it.
      // When that happens, the content should be set to not editable.
      labelBox.dispatchEvent(new FocusEvent('blur', {bubbles: true}));
      assert.isFalse(labelBox.isContentEditable);

      // Double click on the label to make it editable again
      labelBox.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));
      assert.isTrue(labelBox.isContentEditable);
    });
  });

  describe('traceWindowContainingOverlays', () => {
    it('calculates the smallest window that fits the overlay inside', () => {
      const FAKE_EVENT_1 = {
        ts: 0,
        dur: 10,
      } as Trace.Types.Events.Event;
      const FAKE_EVENT_2 = {
        ts: 5,
        dur: 100,
      } as Trace.Types.Events.Event;

      const overlay1: Overlays.Overlays.EntryOutline = {
        entry: FAKE_EVENT_1,
        type: 'ENTRY_OUTLINE',
        outlineReason: 'INFO',
      };
      const overlay2: Overlays.Overlays.EntryOutline = {
        entry: FAKE_EVENT_2,
        type: 'ENTRY_OUTLINE',
        outlineReason: 'INFO',
      };
      const traceWindow = Overlays.Overlays.traceWindowContainingOverlays([overlay1, overlay2]);
      if (!traceWindow) {
        throw new Error('No trace window for overlays');
      }

      assert.strictEqual(traceWindow.min, 0);
      assert.strictEqual(traceWindow.max, 105);
    });

    it('returns null for no overlays', () => {
      const traceWindow = Overlays.Overlays.traceWindowContainingOverlays([]);
      assert.isNull(traceWindow);
    });
  });

  describe('jslogcontext for overlays', () => {
    const FAKE_EVENT = {
      ts: 0,
      dur: 10,
    } as Trace.Types.Events.Event;

    it('does not define a log for an entry_selected overlay', () => {
      const overlay: Overlays.Overlays.EntrySelected = {
        type: 'ENTRY_SELECTED',
        entry: FAKE_EVENT,
      };
      const context = Overlays.Overlays.jsLogContext(overlay);
      assert.isNull(context);
    });

    it('defines a log for an entry outline based on its type', () => {
      const overlayInfo: Overlays.Overlays.EntryOutline = {
        type: 'ENTRY_OUTLINE',
        outlineReason: 'INFO',
        entry: FAKE_EVENT,
      };
      const overlayError: Overlays.Overlays.EntryOutline = {
        type: 'ENTRY_OUTLINE',
        outlineReason: 'ERROR',
        entry: FAKE_EVENT,
      };
      const infoContext = Overlays.Overlays.jsLogContext(overlayInfo);
      assert.strictEqual(infoContext, 'timeline.overlays.entry-outline-info');
      const errorContext = Overlays.Overlays.jsLogContext(overlayError);
      assert.strictEqual(errorContext, 'timeline.overlays.entry-outline-error');
    });

    it('defines a log for entry labels', () => {
      const overlay: Overlays.Overlays.EntryLabel = {
        type: 'ENTRY_LABEL',
        entry: FAKE_EVENT,
        label: 'hello world',
      };
      const context = Overlays.Overlays.jsLogContext(overlay);
      assert.strictEqual(context, 'timeline.overlays.entry-label');
    });

    it('defines a log for time ranges', () => {
      const overlay: Overlays.Overlays.TimeRangeLabel = {
        showDuration: true,
        type: 'TIME_RANGE',
        bounds: microsecondsTraceWindow(1_000, 10_000),
        label: 'hello world',
      };
      const context = Overlays.Overlays.jsLogContext(overlay);
      assert.strictEqual(context, 'timeline.overlays.time-range');
    });

    it('defines a log for timespan breakdowns', () => {
      const overlay: Overlays.Overlays.TimespanBreakdown = {
        type: 'TIMESPAN_BREAKDOWN',
        sections: [],
      };
      const context = Overlays.Overlays.jsLogContext(overlay);
      assert.strictEqual(context, 'timeline.overlays.timespan-breakdown');
    });

    it('defines a log for cursor timestamp marker', () => {
      const overlay: Overlays.Overlays.TimestampMarker = {
        type: 'TIMESTAMP_MARKER',
        timestamp: 1_000 as Trace.Types.Timing.Micro,
      };
      const context = Overlays.Overlays.jsLogContext(overlay);
      assert.strictEqual(context, 'timeline.overlays.cursor-timestamp-marker');
    });

    it('defines a log for candy striped time ranges', () => {
      const overlay: Overlays.Overlays.CandyStripedTimeRange = {
        type: 'CANDY_STRIPED_TIME_RANGE',
        bounds: microsecondsTraceWindow(1_000, 10_000),
        entry: FAKE_EVENT,
      };
      const context = Overlays.Overlays.jsLogContext(overlay);
      assert.strictEqual(context, 'timeline.overlays.candy-striped-time-range');
    });

    it('defines a log for entries links but only if they are connected', () => {
      const overlayConnected: Overlays.Overlays.EntriesLink = {
        type: 'ENTRIES_LINK',
        entryFrom: FAKE_EVENT,
        entryTo: FAKE_EVENT,
        state: Trace.Types.File.EntriesLinkState.CONNECTED,
      };
      const overlayPending: Overlays.Overlays.EntriesLink = {
        type: 'ENTRIES_LINK',
        entryFrom: FAKE_EVENT,
        entryTo: undefined,
        state: Trace.Types.File.EntriesLinkState.PENDING_TO_EVENT,
      };
      const connectedContext = Overlays.Overlays.jsLogContext(overlayConnected);
      assert.strictEqual(connectedContext, 'timeline.overlays.entries-link');

      const pendingContext = Overlays.Overlays.jsLogContext(overlayPending);
      assert.isNull(pendingContext);
    });
  });
});
