/*****************************************************************************
 * 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 { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing';
import { nextTick } from 'vue';

import AutoflowTabularConstants from './AutoflowTabularConstants.js';
import AutoflowTabularPlugin from './AutoflowTabularPlugin.js';
import DOMObserver from './dom-observer.js';

// TODO lots of its without expects
xdescribe('AutoflowTabularPlugin', () => {
  let testType;
  let testObject;
  let mockmct;

  beforeEach(() => {
    testType = 'some-type';
    testObject = { type: testType };
    mockmct = createOpenMct();
    spyOn(mockmct.composition, 'get');
    spyOn(mockmct.objectViews, 'addProvider');
    spyOn(mockmct.telemetry, 'getMetadata');
    spyOn(mockmct.telemetry, 'getValueFormatter');
    spyOn(mockmct.telemetry, 'limitEvaluator');
    spyOn(mockmct.telemetry, 'request');
    spyOn(mockmct.telemetry, 'subscribe');

    const plugin = new AutoflowTabularPlugin({ type: testType });
    plugin(mockmct);
  });

  afterEach(() => {
    return resetApplicationState(mockmct);
  });

  it('installs a view provider', () => {
    expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
  });

  describe('installs a view provider which', () => {
    let provider;

    beforeEach(() => {
      provider = mockmct.objectViews.addProvider.calls.mostRecent().args[0];
    });

    it('applies its view to the type from options', () => {
      expect(provider.canView(testObject, [])).toBe(true);
    });

    it('does not apply to other types', () => {
      expect(provider.canView({ type: 'foo' }, [])).toBe(false);
    });

    describe('provides a view which', () => {
      let testKeys;
      let testChildren;
      let testContainer;
      let testHistories;
      let mockComposition;
      let mockMetadata;
      let mockEvaluator;
      let mockUnsubscribes;
      let callbacks;
      let view;
      let domObserver;

      function waitsForChange() {
        return new Promise(function (resolve) {
          window.requestAnimationFrame(resolve);
        });
      }

      function emitEvent(mockEmitter, type, event) {
        mockEmitter.on.calls.all().forEach((call) => {
          if (call.args[0] === type) {
            call.args[1](event);
          }
        });
      }

      beforeEach(() => {
        callbacks = {};

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

        testObject = { type: 'some-type' };
        testKeys = ['abc', 'def', 'xyz'];
        testChildren = testKeys.map((key) => {
          return {
            identifier: {
              namespace: 'test',
              key: key
            },
            name: 'Object ' + key
          };
        });
        testContainer = document.createElement('div');
        domObserver = new DOMObserver(testContainer);

        testHistories = testKeys.reduce((histories, key, index) => {
          histories[key] = {
            key: key,
            range: index + 10,
            domain: key + index
          };

          return histories;
        }, {});

        mockComposition = jasmine.createSpyObj('composition', ['load', 'on', 'off']);
        mockMetadata = jasmine.createSpyObj('metadata', ['valuesForHints']);

        mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
        mockUnsubscribes = testKeys.reduce((map, key) => {
          map[key] = jasmine.createSpy('unsubscribe-' + key);

          return map;
        }, {});

        mockmct.composition.get.and.returnValue(mockComposition);
        mockComposition.load.and.callFake(() => {
          testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));

          return Promise.resolve(testChildren);
        });

        mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
        mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
          const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
          mockFormatter.format.and.callFake((datum) => {
            return datum[metadatum.hint];
          });

          return mockFormatter;
        });
        mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
        mockmct.telemetry.subscribe.and.callFake((obj, callback) => {
          const key = obj.identifier.key;
          callbacks[key] = callback;

          return mockUnsubscribes[key];
        });
        mockmct.telemetry.request.and.callFake((obj, request) => {
          const key = obj.identifier.key;

          return Promise.resolve([testHistories[key]]);
        });
        mockMetadata.valuesForHints.and.callFake((hints) => {
          return [{ hint: hints[0] }];
        });

        view = provider.view(testObject, [testObject]);
        view.show(testContainer);

        return nextTick();
      });

      afterEach(() => {
        domObserver.destroy();
      });

      it('populates its container', () => {
        expect(testContainer.children.length > 0).toBe(true);
      });

      describe('when rows have been populated', () => {
        function rowsMatch() {
          const rows = testContainer.querySelectorAll('.l-autoflow-row').length;

          return rows === testChildren.length;
        }

        it('shows one row per child object', () => {
          return domObserver.when(rowsMatch);
        });

        // it("adds rows on composition change", () => {
        //     const child = {
        //         identifier: {
        //             namespace: "test",
        //             key: "123"
        //         },
        //         name: "Object 123"
        //     };
        //     testChildren.push(child);
        //     emitEvent(mockComposition, 'add', child);

        //     return domObserver.when(rowsMatch);
        // });

        it('removes rows on composition change', () => {
          const child = testChildren.pop();
          emitEvent(mockComposition, 'remove', child.identifier);

          return domObserver.when(rowsMatch);
        });
      });

      it('removes subscriptions when destroyed', () => {
        testKeys.forEach((key) => {
          expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
        });
        view.destroy();
        testKeys.forEach((key) => {
          expect(mockUnsubscribes[key]).toHaveBeenCalled();
        });
      });

      it('provides a button to change column width', () => {
        const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
        const nextWidth = initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;

        expect(testContainer.querySelector('.l-autoflow-col').css('width')).toEqual(
          initialWidth + 'px'
        );

        testContainer.querySelector('.change-column-width').click();

        function widthHasChanged() {
          const width = testContainer.querySelector('.l-autoflow-col').css('width');

          return width !== initialWidth + 'px';
        }

        return domObserver.when(widthHasChanged).then(() => {
          expect(testContainer.querySelector('.l-autoflow-col').css('width')).toEqual(
            nextWidth + 'px'
          );
        });
      });

      it('subscribes to all child objects', () => {
        testKeys.forEach((key) => {
          expect(callbacks[key]).toEqual(jasmine.any(Function));
        });
      });

      it('displays historical telemetry', () => {
        function rowTextDefined() {
          return testContainer.querySelector('.l-autoflow-item').filter('.r').text() !== '';
        }

        return domObserver.when(rowTextDefined).then(() => {
          testKeys.forEach((key, index) => {
            const datum = testHistories[key];
            const $cell = testContainer.querySelector('.l-autoflow-row').eq(index).find('.r');
            expect($cell.text()).toEqual(String(datum.range));
          });
        });
      });

      it('displays incoming telemetry', () => {
        const testData = testKeys.map((key, index) => {
          return {
            key: key,
            range: index * 100,
            domain: key + index
          };
        });

        testData.forEach((datum) => {
          callbacks[datum.key](datum);
        });

        return waitsForChange().then(() => {
          testData.forEach((datum, index) => {
            const $cell = testContainer.querySelector('.l-autoflow-row').eq(index).find('.r');
            expect($cell.text()).toEqual(String(datum.range));
          });
        });
      });

      it('updates classes for limit violations', () => {
        const testClass = 'some-limit-violation';
        mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
        testKeys.forEach((key) => {
          callbacks[key]({
            range: 'foo',
            domain: 'bar'
          });
        });

        return waitsForChange().then(() => {
          testKeys.forEach((datum, index) => {
            const $cell = testContainer.querySelector('.l-autoflow-row').eq(index).find('.r');
            expect($cell.hasClass(testClass)).toBe(true);
          });
        });
      });

      it('automatically flows to new columns', () => {
        const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
        const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
        const count = testKeys.length;
        const $container = testContainer;
        let promiseChain = Promise.resolve();

        function columnsHaveAutoflowed() {
          const itemsHeight = $container.querySelector('.l-autoflow-items').height();
          const availableHeight = itemsHeight - sliderHeight;
          const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
          const columns = Math.ceil(count / availableRows);

          return $container.querySelector('.l-autoflow-col').length === columns;
        }

        $container.find('.abs').css({
          position: 'absolute',
          left: '0px',
          right: '0px',
          top: '0px',
          bottom: '0px'
        });
        $container.css({ position: 'absolute' });

        $container.appendTo(document.body);

        function setHeight(height) {
          $container.css('height', height + 'px');

          return domObserver.when(columnsHaveAutoflowed);
        }

        for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
          // eslint-disable-next-line no-invalid-this
          promiseChain = promiseChain.then(setHeight.bind(this, height));
        }

        return promiseChain.then(() => {
          $container.remove();
        });
      });

      it('loads composition exactly once', () => {
        const testObj = testChildren.pop();
        emitEvent(mockComposition, 'remove', testObj.identifier);
        testChildren.push(testObj);
        emitEvent(mockComposition, 'add', testObj);
        expect(mockComposition.load.calls.count()).toEqual(1);
      });
    });
  });
});
