describe('Core_loadData', () => {
  const id = 'testContainer';

  beforeEach(function() {
    this.$container = $(`<div id="${id}"></div>`).appendTo('body');
  });

  afterEach(function() {
    if (this.$container) {
      destroy();
      this.$container.remove();
    }
  });

  const arrayOfArrays = function() {
    return [
      ['', 'Kia', 'Nissan', 'Toyota', 'Honda'],
      ['2008', 10, 11, 12, 13],
      ['2009', 20, 11, 14, 13],
      ['2010', 30, 15, 12, 13]
    ];
  };

  const arrayOfObjects = function() {
    return [
      { id: 1, name: 'Ted', lastName: 'Right' },
      { id: 2, name: 'Frank', lastName: 'Honest' },
      { id: 3, name: 'Joan', lastName: 'Well' },
      { id: 4, name: 'Sid', lastName: 'Strong' },
      { id: 5, name: 'Jane', lastName: 'Neat' },
      { id: 6, name: 'Chuck', lastName: 'Jackson' },
      { id: 7, name: 'Meg', lastName: 'Jansen' },
      { id: 8, name: 'Rob', lastName: 'Norris' },
      { id: 9, name: 'Sean', lastName: 'O\'Hara' },
      { id: 10, name: 'Eve', lastName: 'Branson' }
    ];
  };

  const arrayOfNestedObjects = function() {
    return [
      {
        id: 1,
        name: {
          first: 'Ted',
          last: 'Right'
        },
        'full.street': 'Street I',
      },
      {
        id: 2,
        name: {
          first: 'Frank',
          last: 'Honest'
        },
        'full.street': 'Street II',
      },
      {
        id: 3,
        name: {
          first: 'Joan',
          last: 'Well'
        },
        'full.street': 'Street III',
      }
    ];
  };

  const htmlData = [
    ['<b>H&M</b>']
  ];

  it('should allow array of arrays', () => {
    handsontable();
    loadData(arrayOfArrays());
    expect(getDataAtCell(0, 2)).toEqual('Nissan');
  });

  it('should allow array of objects', () => {
    handsontable({
      columns: [
        { data: 'id' },
        { data: 'lastName' },
        { data: 'name' }
      ]
    });
    loadData(arrayOfObjects());
    expect(getDataAtCell(0, 2)).toEqual('Ted');
  });

  it('should allow array of objects when columns as a function', () => {
    handsontable({
      columns(column) {
        let colMeta = {};

        if (column === 0) {
          colMeta.data = 'id';
        } else if (column === 1) {
          colMeta.data = 'lastName';
        } else if (column === 2) {
          colMeta.data = 'name';
        } else {
          colMeta = null;
        }

        return colMeta;
      }
    });
    loadData(arrayOfObjects());
    expect(getDataAtCell(0, 2)).toEqual('Ted');
  });

  it('should allow array of nested objects', () => {
    handsontable({
      data: arrayOfNestedObjects(),
      colHeaders: true,
      columns: [
        { data: 'id' },
        { data: 'name.last' },
        { data: 'name.first' },
        { data: 'full.street' },
      ]
    });
    expect(getDataAtCell(0, 2)).toEqual('Ted');
    expect(getDataAtCell(1, 3)).toEqual('Street II');
    expect(getDataAtRowProp(2, 'full.street')).toEqual('Street III');
  });

  it('should allow array of nested objects when columns as a function', () => {
    handsontable({
      data: arrayOfNestedObjects(),
      colHeaders: true,
      columns(column) {
        let colMeta = {};

        if (column === 0) {
          colMeta.data = 'id';
        } else if (column === 1) {
          colMeta.data = 'name.last';
        } else if (column === 2) {
          colMeta.data = 'name.first';
        } else if (column === 3) {
          colMeta.data = 'full.street';
        } else {
          colMeta = null;
        }

        return colMeta;
      }
    });
    expect(getDataAtCell(0, 2)).toEqual('Ted');
    expect(getDataAtCell(1, 3)).toEqual('Street II');
    expect(getDataAtRowProp(2, 'full.street')).toEqual('Street III');
  });

  it('should figure out default column names for array of nested objects', () => {
    handsontable({
      data: arrayOfNestedObjects(),
      colHeaders: true
    });
    expect(getDataAtCell(0, 2)).toEqual('Right');
  });

  it('should trigger onChange callback when loaded array of arrays', () => {
    let called = false;

    handsontable({
      afterChange(changes, source) {
        if (source === 'loadData') {
          called = true;
        }
      }
    });
    loadData(arrayOfArrays());

    expect(called).toEqual(true);
  });

  it('should trigger onChange callback when loaded array of objects', () => {
    let called = false;

    handsontable({
      afterChange(changes, source) {
        if (source === 'loadData') {
          called = true;
        }
      }
    });
    loadData(arrayOfObjects());

    expect(called).toEqual(true);
  });

  it('should trigger onChange callback when loaded array of nested objects', () => {
    let called = false;

    handsontable({
      afterChange(changes, source) {
        if (source === 'loadData') {
          called = true;
        }
      }
    });
    loadData(arrayOfNestedObjects());

    expect(called).toEqual(true);
  });

  it('should create new rows for array of arrays (and respect minRows)', () => {
    handsontable({
      minRows: 20, // minRows should be respected
      data: arrayOfArrays()
    });

    expect(countRows()).toEqual(20); // TODO why this must be checked after render?
  });

  it('should create new rows for array of nested objects (and respect minRows)', () => {
    handsontable({
      minRows: 20, // minRows should be respected
      data: arrayOfNestedObjects()
    });

    expect(countRows()).toEqual(20); // TODO why this must be checked after render?
  });

  it('HTML special chars should be escaped by default', () => {
    handsontable();
    loadData(htmlData);

    expect(getCell(0, 0).innerHTML).toEqual('&lt;b&gt;H&amp;M&lt;/b&gt;');
  });

  it('should create as many rows as needed by array of objects', () => {
    handsontable({
      minRows: 6,
      data: arrayOfObjects()
    });

    expect(getCell(9, 1).innerHTML).toEqual('Eve');
  });

  // https://github.com/handsontable/handsontable/pull/233
  it('should not invoke the cells callback multiple times with the same row/col (without overlays)', () => {
    const cellsSpy = jasmine.createSpy('cellsSpy');

    handsontable({
      data: arrayOfNestedObjects(),
      colWidths: [90, 90, 90, 90],
      rowHeights: [23, 23, 23, 23],
      cells: cellsSpy
    });
    //
    expect(cellsSpy.calls.count()).toEqual(43);
  });

  it('should not invoke the cells callback multiple times with the same row/col (with overlays)', () => {
    const cellsSpy = jasmine.createSpy('cellsSpy');

    handsontable({
      data: arrayOfNestedObjects(),
      colHeaders: true,
      rowHeaders: true,
      colWidths: [90, 90, 90, 90],
      rowHeights: [90, 90, 90, 90],
      cells: cellsSpy
    });

    expect(cellsSpy.calls.count()).toEqual(56);
  });

  it('should remove grid rows if new data source has less of them', () => {
    const data1 = [
      ['a'],
      ['b'],
      ['c'],
      ['d'],
      ['e'],
      ['f'],
      ['g'],
      ['h']
    ];

    const data2 = [
      ['a'],
      ['b'],
      ['c'],
      ['d'],
      ['e']
    ];

    handsontable({
      data: data1,
      rowHeaders: true,
      colHeaders: true
    });
    selectCell(7, 0);
    loadData(data2);

    expect(countRows()).toBe(data2.length);
    expect(getSelected()).toEqual([[4, 0, 4, 0]]);
  });

  it('should remove grid rows if new data source has less of them (with minSpareRows)', () => {
    const data1 = [
      ['a'],
      ['b'],
      ['c'],
      ['d'],
      ['e'],
      ['f'],
      ['g'],
      ['h']
    ];
    const data2 = [
      ['a'],
      ['b'],
      ['c'],
      ['d'],
      ['e']
    ];

    handsontable({
      data: data1,
      minSpareCols: 1,
      minSpareRows: 1,
      rowHeaders: true,
      colHeaders: true
    });
    selectCell(8, 0);
    loadData(data2);

    expect(countRows()).toBe(6); // +1 because of minSpareRows
    expect(getSelected()).toEqual([[5, 0, 5, 0]]);
  });

  it('loading empty data should remove all rows', () => {
    const data1 = [
      ['a'],
      ['b'],
      ['c'],
      ['d'],
      ['e'],
      ['f'],
      ['g'],
      ['h']
    ];

    const data2 = [];

    handsontable({
      data: data1,
      rowHeaders: true,
      colHeaders: true
    });
    selectCell(7, 0);
    loadData(data2);

    expect(countRows()).toBe(0);
    expect(getSelected()).toBeUndefined();
  });

  it('should only have as many columns as in settings', () => {
    const data1 = arrayOfArrays();

    handsontable({
      data: data1,
      columns: [
        { data: 1 },
        { data: 3 }
      ]
    });

    expect(countCols()).toBe(2);
  });

  it('should only have as many columns as in settings when columns is a function', () => {
    const data1 = arrayOfArrays();

    handsontable({
      data: data1,
      columns(column) {
        let colMeta = {
          data: column
        };

        if ([1, 3].indexOf(column) < 0) {
          colMeta = null;
        }

        return colMeta;
      }
    });

    expect(countCols()).toBe(2);
  });

  it('should throw error when trying to load a string (constructor)', () => {
    let errors = 0;

    try {
      handsontable({
        data: 'string'
      });
    } catch (e) {
      errors += 1;
    }

    expect(errors).toBe(1);
  });

  it('should throw error when trying to load a string (loadData)', () => {
    let errors = 0;

    try {
      handsontable();
      loadData('string');
    } catch (e) {
      errors += 1;
    }

    expect(errors).toBe(1);
  });

  it('should load Backbone Collection as data source', () => {
    // code borrowed from demo/backbone.js

    const CarModel = Backbone.Model.extend({});

    const CarCollection = Backbone.Collection.extend({
      model: CarModel,
      // Backbone.Collection doesn't support `splice`, yet! Easy to add.
      splice: hackedSplice
    });
    const cars = new CarCollection();

    cars.add([
      { make: 'Dodge', model: 'Ram', year: 2012, weight: 6811 },
      { make: 'Toyota', model: 'Camry', year: 2012, weight: 3190 },
      { make: 'Smart', model: 'Fortwo', year: 2012, weight: 1808 }
    ]);

    handsontable({
      data: cars,
      columns: [
        attr('make'),
        attr('model'),
        attr('year')
      ]
    });

    // use the "good" Collection methods to emulate Array.splice
    function hackedSplice(index, howMany, ...models) {
      const args = _.toArray([index, howMany, ...models]).slice(2).concat({ at: index });
      const removed = this.models.slice(index, index + howMany);

      this.remove(removed).add.apply(this, args);

      return removed;
    }

    // normally, you'd get these from the server with .fetch()
    function attr(attribute) {
      // this lets us remember `attr` for when when it is get/set
      return {
        data(car, value) {
          if (_.isUndefined(value)) {
            return car.get(attribute);
          }
          car.set(attribute, value);
        }
      };
    }

    expect(countRows()).toBe(3);
  });

  it('should load Backbone Collection as data source when columns is a function', () => {
    // code borrowed from demo/backbone.js

    const CarModel = Backbone.Model.extend({});

    const CarCollection = Backbone.Collection.extend({
      model: CarModel,
      // Backbone.Collection doesn't support `splice`, yet! Easy to add.
      splice: hackedSplice
    });
    const cars = new CarCollection();

    cars.add([
      { make: 'Dodge', model: 'Ram', year: 2012, weight: 6811 },
      { make: 'Toyota', model: 'Camry', year: 2012, weight: 3190 },
      { make: 'Smart', model: 'Fortwo', year: 2012, weight: 1808 }
    ]);

    handsontable({
      data: cars,
      columns(column) {
        let colMeta = null;

        if (column === 0) {
          colMeta = attr('make');
        } else if (column === 1) {
          colMeta = attr('model');
        } else if (column === 2) {
          colMeta = attr('year');
        }

        return colMeta;
      }
    });

    // use the "good" Collection methods to emulate Array.splice
    function hackedSplice(index, howMany, ...models) {
      const args = _.toArray([index, howMany, ...models]).slice(2).concat({ at: index });
      const removed = this.models.slice(index, index + howMany);

      this.remove(removed).add.apply(this, args);

      return removed;
    }

    // normally, you'd get these from the server with .fetch()
    function attr(attribute) {
      // this lets us remember `attr` for when when it is get/set
      return {
        data(car, value) {
          if (_.isUndefined(value)) {
            return car.get(attribute);
          }

          car.set(attribute, value);
        }
      };
    }

    expect(countRows()).toBe(3);
  });

  it('should clear cell properties after loadData', () => {
    handsontable();
    loadData(arrayOfArrays());

    getCellMeta(0, 0).foo = 'bar';

    expect(getCellMeta(0, 0).foo).toEqual('bar');

    loadData(arrayOfArrays());

    expect(getCellMeta(0, 0).foo).toBeUndefined();
  });

  it('should clear cell properties after loadData, but before rendering new data', () => {
    handsontable();
    loadData(arrayOfArrays());

    getCellMeta(0, 0).valid = false;
    render();

    expect(spec().$container.find('tbody tr:eq(0) td:eq(0)').hasClass('htInvalid')).toEqual(true);

    loadData(arrayOfArrays());

    expect(spec().$container.find('tbody tr:eq(0) td:eq(0)').hasClass('htInvalid')).toEqual(false);

  });

  // https://github.com/handsontable/handsontable/issues/1700
  // can't edit anything after starting editing cell with no nested object
  it('should correct behave with cell with no nested object data source corresponding to column mapping', () => {

    const objectData = [
      { id: 1, user: { name: { first: 'Ted', last: 'Right' } } },
      { id: 2, user: { name: {} } },
      { id: 3 }
    ];

    handsontable({
      data: objectData,
      columns: [
        { data: 'id' },
        { data: 'user.name.first' },
        { data: 'user.name.last' }
      ]
    });

    mouseDoubleClick(getCell(1, 1));
    document.activeElement.value = 'Harry';
    deselectCell();
    expect(objectData[1].user.name.first).toEqual('Harry');

    mouseDoubleClick(getCell(2, 1));
    document.activeElement.value = 'Barry';
    deselectCell();
    expect(objectData[2].user.name.first).toEqual('Barry');
  });

  it('should correct behave with cell with no nested object data source corresponding to column mapping when columns is a function', () => {

    const objectData = [
      { id: 1, user: { name: { first: 'Ted', last: 'Right' } } },
      { id: 2, user: { name: {} } },
      { id: 3 }
    ];

    handsontable({
      data: objectData,
      columns(column) {
        let colMeta = null;

        if (column === 0) {
          colMeta = { data: 'id' };

        } else if (column === 1) {
          colMeta = { data: 'user.name.first' };

        } else if (column === 2) {
          colMeta = { data: 'user.name.last' };
        }

        return colMeta;
      }
    });

    mouseDoubleClick(getCell(1, 1));
    document.activeElement.value = 'Harry';
    deselectCell();
    expect(objectData[1].user.name.first).toEqual('Harry');

    mouseDoubleClick(getCell(2, 1));
    document.activeElement.value = 'Barry';
    deselectCell();
    expect(objectData[2].user.name.first).toEqual('Barry');
  });

});
