import {
  detectSelectionType,
  isValidCoord,
  normalizeSelectionFactory,
  transformSelectionToColumnDistance,
  // TODO: add tests for transformSelectionToRowDistance
  // transformSelectionToRowDistance,
  SELECTION_TYPE_ARRAY,
  SELECTION_TYPE_EMPTY,
  SELECTION_TYPE_OBJECT,
  SELECTION_TYPE_UNRECOGNIZED,
} from 'handsontable/selection/utils';
import { CellRange, CellCoords } from 'walkontable';

describe('selection utils', () => {
  const coords = (row, column) => new CellCoords(row, column);
  const range = (
    rowC,
    columnC,
    rowFrom = rowC,
    columnFrom = columnC,
    rowEnd = rowC,
    columnEnd = columnC
  ) =>
    new CellRange(
      coords(rowC, columnC),
      coords(rowFrom, columnFrom),
      coords(rowEnd, columnEnd)
    );

  describe('detectSelectionType', () => {
    it('should throw an exception when the second argument is being overwritten', () => {
      expect(() => detectSelectionType([1, 1], true)).toThrow();
      expect(() => detectSelectionType([1, 1], false)).toThrow();
      expect(() => detectSelectionType([1, 1], null)).toThrow();
      expect(() => detectSelectionType([1, 1], Symbol(''))).toThrow();
      expect(() => detectSelectionType([1, 1], Symbol('root'))).toThrow();
      expect(() => detectSelectionType([1, 1], Symbol('child'))).toThrow();
    });

    it('should return EMPTY state when the passed data is empty', () => {
      expect(detectSelectionType([])).toBe(SELECTION_TYPE_EMPTY);
      expect(detectSelectionType([[]])).toBe(SELECTION_TYPE_EMPTY);
    });

    it('should return UNRECOGNIZED state when the schema is unrecognized', () => {
      expect(detectSelectionType(null)).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType(1)).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType('z')).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType(true)).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([{}])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType({})).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType()).toBe(SELECTION_TYPE_UNRECOGNIZED);
    });

    it('should return UNRECOGNIZED state when the schema is valid but values are incorect or incomplete', () => {
      expect(detectSelectionType([1])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[1]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType(['prop1'])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([['prop1']])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType(range(1, 2))).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[range(1, 2), range(1, 2)]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[range(1, 2), range(1, 2), range(1, 2)]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[range(1, 2), range(1, 2), range(1, 2), range(1, 2)]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
    });

    it('should ignore nested structures', () => {
      expect(detectSelectionType([[[1, 1]]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[[1, 1, 2, 2]]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[[[1, 1]]]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[[[1, 1, 2, 2]]]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[[[[1, 1]]]]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[[[[1, 1, 2, 2]]]]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
      expect(detectSelectionType([[[[[range(1, 2), range(1, 2)]]]]])).toBe(SELECTION_TYPE_UNRECOGNIZED);
    });

    it('should return ARRAY state on valid an array selection schema', () => {
      expect(detectSelectionType([1, 1])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([1, 1, 2])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([1, 1, 2, 2])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([1, 'prop1'])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([1, 'prop1', 2])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([1, 'prop1', 2, 'prop2'])).toBe(SELECTION_TYPE_ARRAY);

      expect(detectSelectionType([[1, 1]])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([[1, 1, 2]])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([[1, 1, 2, 2]])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([[1, 'prop1']])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([[1, 'prop1', 2]])).toBe(SELECTION_TYPE_ARRAY);
      expect(detectSelectionType([[1, 'prop1', 2, 'prop2']])).toBe(SELECTION_TYPE_ARRAY);
    });

    it('should return OBJECT state on valid range selection schema', () => {
      expect(detectSelectionType([range(1, 2)])).toBe(SELECTION_TYPE_OBJECT);
      expect(detectSelectionType([range(1, 2), range(1, 2)])).toBe(SELECTION_TYPE_OBJECT);
      expect(detectSelectionType([range(1, 2), range(1, 2), range(1, 2)])).toBe(SELECTION_TYPE_OBJECT);
      expect(detectSelectionType([range(1, 2), range(1, 2), range(1, 2), range(1, 2)])).toBe(SELECTION_TYPE_OBJECT);
    });
  });

  describe('normalizeSelectionFactory', () => {
    it('should throw an exception on invalid type', () => {
      expect(() => normalizeSelectionFactory()).toThrow();
      expect(() => normalizeSelectionFactory(0)).toThrow();
      expect(() => normalizeSelectionFactory('0')).toThrow();
      expect(() => normalizeSelectionFactory(null)).toThrow();
      expect(() => normalizeSelectionFactory('foo')).toThrow();
    });

    it('should create normalizer function', () => {
      const normalizer = normalizeSelectionFactory(SELECTION_TYPE_ARRAY);

      expect(normalizer).toBeFunction();
    });

    it('should create ARRAY normalizer function with default options (it modifies coordinates direction)', () => {
      const normalizer = normalizeSelectionFactory(SELECTION_TYPE_ARRAY);

      expect(normalizer([1, 1])).toEqual([1, 1, 1, 1]);
      expect(normalizer([1, 1, 2])).toEqual([1, 1, 2, 1]);
      expect(normalizer([1, 1, 2, 2])).toEqual([1, 1, 2, 2]);
      expect(normalizer([2, 2, 1, 1])).toEqual([1, 1, 2, 2]);
      expect(normalizer([2, 'prop2', 1, 'prop1'])).toEqual([1, NaN, 2, NaN]);
    });

    it('should create OBJECT normalizer function with default options (it modifies coordinates direction)', () => {
      const normalizer = normalizeSelectionFactory(SELECTION_TYPE_OBJECT);

      expect(normalizer(range(1, 1))).toEqual([1, 1, 1, 1]);
      expect(normalizer(range(1, 1, 2, 2))).toEqual([1, 1, 2, 2]);
      expect(normalizer(range(2, 2, 1, 1))).toEqual([1, 1, 2, 2]);
    });

    it('should create ARRAY normalizer function which keep origin coordinates direction', () => {
      const normalizer = normalizeSelectionFactory(SELECTION_TYPE_ARRAY, { keepDirection: true });

      expect(normalizer([1, 1, 2, 2])).toEqual([1, 1, 2, 2]);
      expect(normalizer([2, 2, 1, 1])).toEqual([2, 2, 1, 1]);
    });

    it('should create OBJECT normalizer function which keep origin coordinates direction', () => {
      const normalizer = normalizeSelectionFactory(SELECTION_TYPE_OBJECT, { keepDirection: true });

      expect(normalizer(range(1, 1))).toEqual([1, 1, 1, 1]);
      expect(normalizer(range(1, 1, 2, 2))).toEqual([2, 2, 1, 1]);
      expect(normalizer(range(2, 2, 1, 1))).toEqual([1, 1, 2, 2]);
    });

    it('should create ARRAY normalizer function which translates column string coordinates to visual indexes', () => {
      const propToColMap = new Map([['prop0', 9], ['prop1', 8], ['prop2', 7], ['prop3', 6]]);
      const propToCol = prop => propToColMap.get(prop);
      const normalizer = normalizeSelectionFactory(SELECTION_TYPE_ARRAY, { propToCol });

      expect(normalizer([1, 1, 2, 2])).toEqual([1, 1, 2, 2]);
      expect(normalizer([1, 'prop1', 2, 3])).toEqual([1, 3, 2, 8]);
      expect(normalizer([1, 'prop1', 2, 'prop2'])).toEqual([1, 7, 2, 8]);
    });
  });

  describe('transformSelectionToColumnDistance', () => {
    it('should return an empty array when selection schema is unrecoginized', () => {
      expect(transformSelectionToColumnDistance()).toEqual([]);
      expect(transformSelectionToColumnDistance(0)).toEqual([]);
      expect(transformSelectionToColumnDistance(true)).toEqual([]);
      expect(transformSelectionToColumnDistance([])).toEqual([]);
      expect(transformSelectionToColumnDistance([1])).toEqual([]);
      expect(transformSelectionToColumnDistance([[1], [3, 3, 3, 5]])).toEqual([]);
    });

    it('should translate selection ranges passed as an array of arrays to column distances', () => {
      expect(transformSelectionToColumnDistance([[1, 1, 2, 2]])).toEqual([[1, 2]]);
      expect(transformSelectionToColumnDistance([[1, 1], [3, 3, 3, 5]])).toEqual([[1, 1], [3, 3]]);
      expect(transformSelectionToColumnDistance([[1, 1], [3, 3, 3, 5], [5, 1, 6, 3]])).toEqual([[1, 5]]);
      expect(transformSelectionToColumnDistance([[1, 1], [3, 3, 3, 5], [5, 1, 6, 3], [5, 7, 5, 7]])).toEqual([[1, 5], [7, 1]]);
    });

    it('should translate selection ranges passed as an array of CellRange objects to column distances', () => {
      expect(transformSelectionToColumnDistance([range(1, 1, 1, 1, 2, 2)])).toEqual([[1, 2]]);
      expect(transformSelectionToColumnDistance([range(1, 1, 1, 1, 2, 2), range(3, 3, 3, 3, 3, 5)])).toEqual([[1, 5]]);
    });
  });

  describe('isValidCoord', () => {
    it('should return `false` on invalid coordinates', () => {
      expect(isValidCoord()).toBe(false);
      expect(isValidCoord(-1)).toBe(false);
      expect(isValidCoord(null)).toBe(false);
      expect(isValidCoord(void 0)).toBe(false);
      expect(isValidCoord('0')).toBe(false);
      expect(isValidCoord('a')).toBe(false);
      expect(isValidCoord([1])).toBe(false);
      expect(isValidCoord({ foo: 1 })).toBe(false);
    });

    it('should return `true` on valid coordinates', () => {
      expect(isValidCoord(0)).toBe(true);
      expect(isValidCoord(1)).toBe(true);
      expect(isValidCoord(100)).toBe(true);
      expect(isValidCoord(10000000)).toBe(true);
    });

    it('should return `true` when coordinates are included in the range 0 to `maxItems`', () => {
      const maxItems = 100;

      expect(isValidCoord(0, maxItems)).toBe(true);
      expect(isValidCoord(1, maxItems)).toBe(true);
      expect(isValidCoord(99, maxItems)).toBe(true);
      expect(isValidCoord(99.999, maxItems)).toBe(true);
    });

    it('should return `false` when coordinates are not included in the range 0 to `maxItems`', () => {
      const maxItems = 100;

      expect(isValidCoord(100.000001, maxItems)).toBe(false);
      expect(isValidCoord(101, maxItems)).toBe(false);
      expect(isValidCoord(9999, maxItems)).toBe(false);
    });
  });
});
