/* eslint no-underscore-dangle:0 */
import * as ObjectConversionModule from '@nebula.js/conversion';
import create from '../viz';
import * as glueModule from '../components/glue';
import * as getPatchesModule from '../utils/patcher';
import * as validatePluginsModule from '../plugins/plugins';

describe('viz', () => {
  let api;
  let glue;
  let mounted;
  let model;
  let getPatches;
  let cellRef;
  let validatePlugins;

  let unmountMock;
  let setSnOptions;
  let setSnContext;
  let setSnPlugins;
  let getExtensionDefinition;
  let setModel;
  let takeSnapshot;
  let exportImage;
  let getImperativeHandle;
  let convertToMock;
  let createSessionObjectMock;
  let destroySessionObjectMock;

  let mockElement;

  let definitionName = 'props';

  beforeAll(() => {
    unmountMock = jest.fn();
    setSnOptions = jest.fn();
    setSnContext = jest.fn();
    setSnPlugins = jest.fn();
    getExtensionDefinition = jest.fn().mockImplementation(() => ({ definition: { name: definitionName } }));
    setModel = jest.fn();
    takeSnapshot = jest.fn();
    exportImage = jest.fn();
    getImperativeHandle = jest.fn(async () => ({
      api: 'api',
    }));
    cellRef = {
      current: {
        setSnOptions,
        setSnContext,
        setSnPlugins,
        getExtensionDefinition,
        setModel,
        takeSnapshot,
        exportImage,
        getImperativeHandle,
      },
    };
    glue = jest.fn().mockReturnValue([unmountMock, cellRef]);
    getPatches = jest.fn().mockReturnValue(['patch']);
    convertToMock = jest.fn().mockReturnValue('props');
    validatePlugins = jest.fn();
    createSessionObjectMock = jest.fn().mockReturnValue({ id: 'newModelId' });
    destroySessionObjectMock = jest.fn();

    jest.spyOn(glueModule, 'default').mockImplementation(glue);
    jest.spyOn(getPatchesModule, 'default').mockImplementation(getPatches);
    jest.spyOn(ObjectConversionModule, 'convertTo').mockImplementation(convertToMock);
    jest.spyOn(validatePluginsModule, 'default').mockImplementation(validatePlugins);

    model = {
      getEffectiveProperties: jest.fn().mockReturnValue('old'),
      getLayout: jest.fn().mockReturnValue({ qMeta: { privileges: ['update'] } }),
      applyPatches: jest.fn(),
      on: jest.fn(),
      once: jest.fn(),
      emit: jest.fn(),
      setProperties: jest.fn(),
      id: 'uid',
    };
    api = create({
      model,
      halo: {
        public: {},
        context: { dataViewType: 'sn-table', enablePrivateExperimental: true },
        app: { createSessionObject: createSessionObjectMock, destroySessionObject: destroySessionObjectMock },
        types: { getSupportedVersion: () => true },
      },
    });

    mockElement = document.createElement('div');
  });
  afterAll(() => {
    jest.resetAllMocks();
    jest.restoreAllMocks();
  });

  describe('public api', () => {
    test('should have an id', () => {
      expect(typeof api.id).toBe('string');
    });

    test('should have a destroy method', () => {
      expect(typeof api.destroy).toBe('function');
    });
  });
  describe('internal api', () => {
    test('should have an applyProperties method', () => {
      expect(typeof api.__DO_NOT_USE__.applyProperties).toBe('function');
    });

    test('should have an exportImage method', () => {
      expect(typeof api.__DO_NOT_USE__.exportImage).toBe('function');
    });

    test('should have an convertTo method', () => {
      expect(typeof api.convertTo).toBe('function');
    });
  });

  describe('mounting', () => {
    test('show throw for invalid elements', () => {
      const t = () => {
        api.__DO_NOT_USE__.mount('just a string');
      };
      expect(t).toThrow('Provided element is not a proper HTMLElement');
    });

    test('should mount', async () => {
      mounted = api.__DO_NOT_USE__.mount(mockElement);
      const { onMount } = glue.mock.lastCall[0];
      onMount();
      await mounted;
      expect(glue).toHaveBeenCalledTimes(1);
    });
    test('should throw if already mounted', async () => {
      const t = () => {
        mounted = api.__DO_NOT_USE__.mount(mockElement);
      };
      expect(t).toThrow('Already mounted');
    });
  });

  describe('applyProperties', () => {
    test('should apply patches when there are some', async () => {
      await api.__DO_NOT_USE__.applyProperties('new');
      expect(model.getEffectiveProperties).toHaveBeenCalledTimes(1);
      expect(getPatches).toHaveBeenCalledWith('/', 'new', 'old');
      expect(model.applyPatches).toHaveBeenCalledWith(['patch'], true);
    });

    // TODO:
    // in original test case, it was mocking model.getEffectiveProperties, we do it here too
    // but this test case uses a method on that mocked property (in resetHistory() call) and the problem is exactly there.
    // if we soppoused to mock a function, it will be mocked entierly, and we will not have access to it's methods
    // one way would be to mock what ever it returns, inbcluding resetHistory as well, but it is not applicable in this test case
    // because seems like this test cases expects to have some previously stored state from previous tests
    // that needs to be cleared by calling the actual method!
    test.skip('should not apply patches when there is no diff', async () => {
      model.getEffectiveProperties.resetHistory();
      await api.__DO_NOT_USE__.applyProperties('new');
      getPatches.mockReturnValue([]);
      model.applyPatches.resetHistory();
      expect(model.getEffectiveProperties).toHaveBeenCalledTimes(2);
      expect(getPatches).toHaveBeenCalledWith('/', 'new', 'old');
      expect(model.applyPatches).toHaveBeenCalledTimes(0);
    });
  });

  describe('destroy', () => {
    test('should cleanup', async () => {
      await api.destroy();
      expect(unmountMock).toHaveBeenCalledWith();
    });
  });

  describe('options', () => {
    test('should set sn options', async () => {
      const opts = {};
      api.__DO_NOT_USE__.options(opts);
      await mounted;
      const args = cellRef.current.setSnOptions.mock.lastCall[0];
      expect(args.onInitialRender).toBeInstanceOf(Function);
      expect(Object.keys(args)).toHaveLength(1);
    });

    test('should set extended sn options', async () => {
      const opts = { myops: 'myopts' };
      api.__DO_NOT_USE__.options(opts);
      await mounted;
      const args = cellRef.current.setSnOptions.mock.lastCall[0];
      expect(args.onInitialRender).toBeInstanceOf(Function);
      expect(args.myops).toEqual('myopts');
      expect(Object.keys(args)).toHaveLength(2);
    });

    test('should override and call onIntialRender', async () => {
      const opts = { myops: 'myopts', onInitialRender: jest.fn() };
      api.__DO_NOT_USE__.options(opts);
      await mounted;
      const args = cellRef.current.setSnOptions.mock.lastCall[0];
      args.onInitialRender();
      expect(opts.onInitialRender).toHaveBeenCalled();
    });
  });

  describe('plugins', () => {
    test('should set sn plugins', async () => {
      const plugins = [{ info: { name: 'testplugin' }, fn: () => {} }];
      api.__DO_NOT_USE__.plugins(plugins);
      await mounted;
      expect(cellRef.current.setSnPlugins).toHaveBeenCalledWith(plugins);
    });

    test('should validate plugins', async () => {
      const plugins = [{ info: { name: 'testplugin' }, fn: () => {} }];
      api.__DO_NOT_USE__.plugins(plugins);
      await mounted;
      expect(validatePlugins).toHaveBeenCalledWith(plugins);
    });
  });

  describe('snapshot', () => {
    test('should take a snapshot', async () => {
      api.__DO_NOT_USE__.takeSnapshot();
      expect(cellRef.current.takeSnapshot).toHaveBeenCalledWith();
    });
  });

  describe('export', () => {
    test('should export image', async () => {
      api.__DO_NOT_USE__.exportImage();
      expect(cellRef.current.exportImage).toHaveBeenCalledWith();
    });
  });

  describe('convertTo', () => {
    test('should run setProperties when forceUpdate = true', async () => {
      const props = await api.convertTo('type', true);
      expect(convertToMock).toHaveBeenCalledTimes(1);
      expect(model.setProperties).toHaveBeenCalledTimes(1);
      expect(props).toBe('props');
    });

    test('should not run setProperties when forceUpdate = false', async () => {
      const props = await api.convertTo('type', false);
      expect(convertToMock).toHaveBeenCalledTimes(1);
      expect(model.setProperties).toHaveBeenCalledTimes(0);
      expect(props).toBe('props');
    });
  });

  describe('toggleDataView', () => {
    test('should toggle data view and back', async () => {
      await api.toggleDataView();
      expect(convertToMock).toHaveBeenCalledWith(expect.objectContaining({ viewDataMode: true, newType: 'sn-table' }));
      expect(createSessionObjectMock).toHaveBeenCalledTimes(1);
      await api.toggleDataView();
      expect(destroySessionObjectMock).toHaveBeenCalledWith('newModelId');
      expect(setModel).toHaveBeenCalledTimes(2);
    });

    test('should not toggle data view when showDataView is being used', async () => {
      await api.toggleDataView(false);
      expect(createSessionObjectMock).toHaveBeenCalledTimes(0);
      await api.toggleDataView(true);
      expect(createSessionObjectMock).toHaveBeenCalledTimes(1);
      expect(setModel).toHaveBeenCalledTimes(1);
      await api.toggleDataView(true);
      expect(destroySessionObjectMock).toHaveBeenCalledTimes(0);
      await api.toggleDataView(false);
      expect(destroySessionObjectMock).toHaveBeenCalledTimes(1);
      expect(setModel).toHaveBeenCalledTimes(2);
    });
  });

  describe('getImperativeHandle', () => {
    test('should await the rendering and then call cell', async () => {
      const opts = { myops: 'myopts', onInitialRender: jest.fn() };
      api.__DO_NOT_USE__.options(opts);
      await mounted;
      const args = cellRef.current.setSnOptions.mock.lastCall[0];
      args.onInitialRender();
      const handle = await api.getImperativeHandle();
      expect(handle.api).toEqual('api');
    });
  });

  describe('getPropertyPanelDefinition', () => {
    test('should fetch the definition.ext.definition', async () => {
      const opts = { myops: 'myopts', onInitialRender: jest.fn() };
      api.__DO_NOT_USE__.options(opts);
      await mounted;
      const args = cellRef.current.setSnOptions.mock.lastCall[0];
      args.onInitialRender();
      const panelDef = api.getPropertyPanelDefinition();
      expect(panelDef.name).toEqual('props');
    });

    test('should return original after data view toggle the definition.ext.definition', async () => {
      const opts = { myops: 'myopts', onInitialRender: jest.fn() };
      api.__DO_NOT_USE__.options(opts);
      await mounted;
      const args = cellRef.current.setSnOptions.mock.lastCall[0];
      args.onInitialRender();
      let panelDef = api.getPropertyPanelDefinition();
      expect(panelDef.name).toEqual('props');
      definitionName = 'old';
      await api.toggleDataView(); // locks in the "old" definition
      definitionName = 'new';
      panelDef = api.getPropertyPanelDefinition();
      expect(panelDef.name).toEqual('old');
    });
  });
});
