/*****************************************************************************
 * Open MCT, Copyright (c) 2014-2024, United States Government
 * as represented by the Administrator of the National Aeronautics and Space
 * Administration. All rights reserved.
 *
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 * Open MCT includes source code licensed under additional open source
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 * this source code distribution or the Licensing information page available
 * at runtime from the About dialog for additional information.
 *****************************************************************************/
import {
  createMouseEvent,
  createOpenMct,
  renderWhenVisible,
  resetApplicationState,
  spyOnBuiltins
} from 'utils/testing';
import { nextTick } from 'vue';

import { MODE } from './constants.js';
import TablePlugin from './plugin.js';

class MockDataTransfer {
  constructor() {
    this.data = {};
  }
  get types() {
    return Object.keys(this.data);
  }
  setData(format, data) {
    this.data[format] = data;
  }
  getData(format) {
    return this.data[format];
  }
}

describe('the plugin', () => {
  let openmct;
  let tablePlugin;
  let element;
  let child;
  let historicalTelemetryProvider;
  let originalRouterPath;
  let unlistenConfigMutation;

  beforeEach((done) => {
    openmct = createOpenMct();

    // Table Plugin is actually installed by default, but because installing it
    // again is harmless it is left here as an example for non-default plugins.
    tablePlugin = new TablePlugin();
    openmct.install(tablePlugin);

    historicalTelemetryProvider = {
      request: () => {
        return Promise.resolve([]);
      }
    };
    spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalTelemetryProvider);

    element = document.createElement('div');
    child = document.createElement('div');
    element.appendChild(child);

    openmct.time.timeSystem('utc', {
      start: 0,
      end: 4
    });

    openmct.types.addType('test-object', {
      creatable: true
    });

    spyOnBuiltins(['requestAnimationFrame']);
    window.requestAnimationFrame.and.callFake((callBack) => {
      callBack();
    });

    originalRouterPath = openmct.router.path;

    openmct.on('start', done);
    openmct.startHeadless();
  });

  afterEach(() => {
    openmct.time.timeSystem('utc', {
      start: 0,
      end: 1
    });

    if (unlistenConfigMutation) {
      unlistenConfigMutation();
    }

    return resetApplicationState(openmct);
  });

  describe('defines a table object', function () {
    it('that is creatable', () => {
      let tableType = openmct.types.get('table');
      expect(tableType.definition.creatable).toBe(true);
    });
  });

  it('provides a table view for objects with telemetry', () => {
    const testTelemetryObject = {
      id: 'test-object',
      type: 'test-object',
      telemetry: {
        values: [
          {
            key: 'some-key'
          }
        ]
      }
    };

    const applicableViews = openmct.objectViews.get(testTelemetryObject, []);
    let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table');
    expect(tableView).toBeDefined();
  });

  describe('The table view', () => {
    let testTelemetryObject;
    let applicableViews;
    let tableViewProvider;
    let tableView;
    let tableInstance;
    let mockClock;
    let telemetryCallback;

    beforeEach(async () => {
      openmct.time.timeSystem('utc', {
        start: 0,
        end: 10
      });

      mockClock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);
      mockClock.key = 'mockClock';
      mockClock.currentValue.and.returnValue(1);

      openmct.time.addClock(mockClock);
      openmct.time.clock('mockClock', {
        start: 0,
        end: 10
      });

      testTelemetryObject = {
        identifier: {
          namespace: '',
          key: 'test-object'
        },
        type: 'test-object',
        name: 'Test Object',
        telemetry: {
          values: [
            {
              key: 'utc',
              format: 'utc',
              name: 'Time',
              hints: {
                domain: 1
              }
            },
            {
              key: 'some-key',
              name: 'Some attribute',
              hints: {
                range: 1
              }
            },
            {
              key: 'some-other-key',
              name: 'Another attribute',
              hints: {
                range: 2
              }
            }
          ]
        },
        configuration: {
          hiddenColumns: {
            name: false,
            utc: false,
            'some-key': false,
            'some-other-key': false
          },
          persistModeChange: true,
          rowLimit: 50,
          telemetryMode: MODE.PERFORMANCE
        }
      };
      const testTelemetry = [
        {
          utc: 1,
          'some-key': 'some-value 1',
          'some-other-key': 'some-other-value 1'
        },
        {
          utc: 2,
          'some-key': 'some-value 2',
          'some-other-key': 'some-other-value 2'
        },
        {
          utc: 3,
          'some-key': 'some-value 3',
          'some-other-key': 'some-other-value 3'
        }
      ];

      historicalTelemetryProvider.request = () => {
        return Promise.resolve(testTelemetry);
      };

      const realtimeTelemetryProvider = {
        supportsSubscribe: () => true,
        subscribe: (domainObject, passedCallback) => {
          telemetryCallback = passedCallback;
          return Promise.resolve(() => {});
        }
      };

      spyOn(openmct.telemetry, 'findSubscriptionProvider').and.returnValue(
        realtimeTelemetryProvider
      );

      openmct.router.path = [testTelemetryObject];

      applicableViews = openmct.objectViews.get(testTelemetryObject, []);
      tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
      tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
      tableView.show(child, true, { renderWhenVisible });

      tableInstance = tableView.getTable();

      await nextTick();
    });

    afterEach(() => {
      openmct.router.path = originalRouterPath;
      openmct.time.setClock('local');
    });

    it('Shows no progress bar initially', () => {
      let progressBar = element.querySelector('.c-progress-bar');

      expect(tableInstance.outstandingRequests).toBe(0);
      expect(progressBar).toBeNull();
    });

    it('Shows a progress bar while making requests', async () => {
      tableInstance.incrementOutstandingRequests();
      await nextTick();

      let progressBar = element.querySelector('.c-progress-bar');

      expect(tableInstance.outstandingRequests).toBe(1);
      expect(progressBar).not.toBeNull();
    });

    it('Renders a row for every telemetry datum returned', async () => {
      let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
      await nextTick();
      expect(rows.length).toBe(3);
    });

    it('Adds a row in place when updating with existing telemetry', async () => {
      let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
      await nextTick();
      expect(rows.length).toBe(3);
      // fire some telemetry
      const newTelemetry = {
        utc: 2,
        'some-key': 'some-value 2',
        'some-other-key': 'spacecraft'
      };
      spyOn(tableInstance.tableRows, 'getInPlaceUpdateIndex').and.returnValue(1);
      spyOn(tableInstance.tableRows, 'updateRowInPlace').and.callThrough();
      telemetryCallback(newTelemetry);

      expect(tableInstance.tableRows.updateRowInPlace.calls.count()).toBeGreaterThan(0);
    });

    it('Renders a column for every item in telemetry metadata', () => {
      let headers = element.querySelectorAll('span.c-telemetry-table__headers__label');
      expect(headers.length).toBe(4);
      expect(headers[0].innerText).toBe('Name');
      expect(headers[1].innerText).toBe('Time');
      expect(headers[2].innerText).toBe('Some attribute');
      expect(headers[3].innerText).toBe('Another attribute');
    });

    it('Supports column reordering via drag and drop', async () => {
      let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
      let fromColumn = columns[0];
      let toColumn = columns[1];
      let fromColumnText = fromColumn.querySelector(
        'span.c-telemetry-table__headers__label'
      ).innerText;
      let toColumnText = toColumn.querySelector('span.c-telemetry-table__headers__label').innerText;

      let dragStartEvent = createMouseEvent('dragstart');
      let dragOverEvent = createMouseEvent('dragover');
      let dropEvent = createMouseEvent('drop');

      dragStartEvent.dataTransfer =
        dragOverEvent.dataTransfer =
        dropEvent.dataTransfer =
          new MockDataTransfer();

      fromColumn.dispatchEvent(dragStartEvent);
      toColumn.dispatchEvent(dragOverEvent);
      toColumn.dispatchEvent(dropEvent);

      await nextTick();
      columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
      let firstColumn = columns[0];
      let secondColumn = columns[1];
      let firstColumnText = firstColumn.querySelector(
        'span.c-telemetry-table__headers__label'
      ).innerText;
      let secondColumnText = secondColumn.querySelector(
        'span.c-telemetry-table__headers__label'
      ).innerText;
      expect(fromColumnText).not.toEqual(firstColumnText);
      expect(fromColumnText).toEqual(secondColumnText);
      expect(toColumnText).not.toEqual(secondColumnText);
      expect(toColumnText).toEqual(firstColumnText);
    });

    it('displays the correct number of column headers when the configuration is mutated', async () => {
      const tableInstanceConfiguration = tableInstance.domainObject.configuration;
      tableInstanceConfiguration.hiddenColumns['some-key'] = true;
      unlistenConfigMutation = tableInstance.openmct.objects.mutate(
        tableInstance.domainObject,
        'configuration',
        tableInstanceConfiguration
      );

      await nextTick();
      let tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label');
      expect(tableHeaderElements.length).toEqual(3);

      tableInstanceConfiguration.hiddenColumns['some-key'] = false;
      unlistenConfigMutation = tableInstance.openmct.objects.mutate(
        tableInstance.domainObject,
        'configuration',
        tableInstanceConfiguration
      );

      await nextTick();
      tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label');
      expect(tableHeaderElements.length).toEqual(4);
    });

    it('displays the correct number of table cells in a row when the configuration is mutated', async () => {
      const tableInstanceConfiguration = tableInstance.domainObject.configuration;
      tableInstanceConfiguration.hiddenColumns['some-key'] = true;
      unlistenConfigMutation = tableInstance.openmct.objects.mutate(
        tableInstance.domainObject,
        'configuration',
        tableInstanceConfiguration
      );

      await nextTick();
      let tableRowCells = element.querySelectorAll(
        'table.c-telemetry-table__body > tbody > tr:first-child td'
      );
      expect(tableRowCells.length).toEqual(3);

      tableInstanceConfiguration.hiddenColumns['some-key'] = false;
      unlistenConfigMutation = tableInstance.openmct.objects.mutate(
        tableInstance.domainObject,
        'configuration',
        tableInstanceConfiguration
      );

      await nextTick();
      tableRowCells = element.querySelectorAll(
        'table.c-telemetry-table__body > tbody > tr:first-child td'
      );
      expect(tableRowCells.length).toEqual(4);
    });

    it('Pauses the table when a row is marked', async () => {
      let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr');
      let clickEvent = createMouseEvent('click');

      // Mark a row
      firstRow.dispatchEvent(clickEvent);

      await nextTick();

      // Verify table is paused
      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
    });

    it('Unpauses the table on user bounds change', async () => {
      let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr');
      let clickEvent = createMouseEvent('click');

      // Mark a row
      firstRow.dispatchEvent(clickEvent);

      await nextTick();

      // Verify table is paused
      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();

      const currentBounds = openmct.time.bounds();
      await nextTick();
      const newBounds = {
        start: currentBounds.start,
        end: currentBounds.end - 3
      };

      // Manually change the time bounds
      openmct.time.bounds(newBounds);
      await nextTick();

      // Verify table is no longer paused
      expect(element.querySelector('div.c-table.is-paused')).toBeNull();
    });

    it('Unpauses the table on user bounds change if paused by button', async () => {
      const viewContext = tableView.getViewContext();

      // Pause by button
      viewContext.togglePauseByButton();
      await nextTick();

      // Verify table is paused
      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();

      const currentBounds = openmct.time.bounds();
      await nextTick();

      const newBounds = {
        start: currentBounds.start,
        end: currentBounds.end - 1
      };
      // Manually change the time bounds
      openmct.time.bounds(newBounds);

      await nextTick();

      // Verify table is no longer paused
      expect(element.querySelector('div.c-table.is-paused')).toBeNull();
    });

    it('Does not unpause the table on tick', async () => {
      const viewContext = tableView.getViewContext();

      // Pause by button
      viewContext.togglePauseByButton();

      await nextTick();

      // Verify table displays the correct number of rows
      let tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
      expect(tableRows.length).toEqual(3);

      // Verify table is paused
      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();

      // Tick the clock
      openmct.time.tick(1);

      await nextTick();

      // Verify table is still paused
      expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();

      await nextTick();

      // Verify table displays the correct number of rows
      tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
      expect(tableRows.length).toEqual(3);
    });
  });
});
