/* globals describe, it, beforeEach, afterEach */
/* jshint expr: true */

'use strict';

var expect = window.chai.expect;
var sinon = window.sinon;

describe('shuffle', function () {

  var Shuffle = window.shuffle;
  var fixtures = {};
  var fixture;
  var instance;

  function getFixture(id) {
    return new Promise(function (resolve, reject) {
      var xhr = new XMLHttpRequest();
      xhr.onload = function () {
        resolve(xhr.responseText);
      };

      xhr.onerror = reject;

      xhr.open('GET', 'fixtures/' + id + '.html');
      xhr.send();
    });
  }

  function appendFixture(id) {

    function insert(content) {
      content = content.trim();
      var holder = document.createElement('div');
      holder.innerHTML = content;
      var element = holder.firstChild;
      document.body.appendChild(element);
      fixture = element;
    }

    if (fixtures[id]) {
      insert(fixtures[id]);
      return Promise.resolve();
    } else {
      return getFixture(id).then(function (html) {
        fixtures[id] = html;
        insert(html);
      });
    }
  }

  function removeFixture() {
    if (instance && instance.element) {
      instance.destroy();
    }

    if (fixture) {
      fixture.parentNode.removeChild(fixture);
    }

    instance = null;
    fixture = null;
  }

  function once(element, eventType, fn) {
    var handler = function (e) {
      element.removeEventListener(eventType, handler);
      fn(e);
    };

    element.addEventListener(eventType, handler);
  }

  function toArray(thing) {
    return Array.prototype.slice.call(thing);
  }

  function id(id) {
    return document.getElementById(id);
  }

  describe('regular fixture', function () {

    beforeEach(function (done) {
      // Mock the transition end event wrapper.
      sinon.stub(Shuffle.prototype, '_whenTransitionDone', function (element, itemCallback, done) {
        setTimeout(done, 0);
      });

      appendFixture('regular').then(done);
    });

    afterEach(function () {
      Shuffle.prototype._whenTransitionDone.restore();
      removeFixture();
    });

    it('should have default options', function () {
      instance = new Shuffle(fixture);
      expect(instance.items.length).to.equal(10);
      expect(instance.visibleItems).to.equal(10);
      expect(instance.options.group).to.equal('all');
      expect(instance.options.speed).to.equal(250);
      expect(instance.options.itemSelector).to.equal('*');
      expect(instance.options.sizer).to.equal(null);
      expect(instance.options.columnWidth).to.equal(0);
      expect(instance.options.gutterWidth).to.equal(0);
      expect(instance.options.delimeter).to.equal(null);
      expect(instance.options.initialSort).to.equal(null);
      expect(instance.options.throttleTime).to.equal(300);
      expect(instance.useSizer).to.equal(false);
      expect(instance.id).to.equal('shuffle_0');

      expect(instance.isInitialized).to.be.true;
    });

    it('should add classes and default styles', function () {
      instance = new Shuffle(fixture);
      expect(instance.element).to.have.class('shuffle');

      var styles = window.getComputedStyle(instance.element, null);
      expect(styles.position).to.equal('relative');
      expect(styles.overflow).to.equal('hidden');
      expect(instance.containerWidth).not.to.equal(0);

      instance.items.forEach(function (item) {
        expect(item.element).to.have.class('shuffle-item');
        expect(item.element).to.have.class('shuffle-item--visible');
        expect(item.element.style.opacity).to.be.defined;
        expect(item.element.style.position).to.equal('absolute');
        expect(item.element.style.visibility).to.equal('visible');
        expect(item.isVisible).to.equal(true);
        expect(item.scale).to.equal(Shuffle.ShuffleItem.Scale.VISIBLE);
        expect(item.point.x).to.be.defined;
        expect(item.point.y).to.be.defined;
      });
    });

    it('should be 3 columns with gutters', function () {
      fixture.style.width = '1000px';

      for (var i = 0; i < fixture.children.length; i++) {
        fixture.children[i].style.width = '300px';
        fixture.children[i].style.height = '150px';
      }

      instance = new Shuffle(fixture, {
        columnWidth: 300,
        gutterWidth: 50,
      });

      expect(instance.colWidth).to.equal(350);
      expect(instance.cols).to.equal(3);
      expect(instance.positions).to.deep.equal([600, 450, 450]);
    });

    it('can have a function for columns and gutters', function () {
      fixture.style.width = '1000px';

      for (var i = 0; i < fixture.children.length; i++) {
        fixture.children[i].style.width = '300px';
        fixture.children[i].style.height = '150px';
      }

      instance = new Shuffle(fixture, {
        columnWidth: function (containerWidth) {
          expect(containerWidth).to.equal(1000);
          return 300;
        },

        gutterWidth: function () {
          return 50;
        },
      });

      expect(instance._getGutterSize(1000)).to.equal(50);
      expect(instance._getColumnSize(1000, 50)).to.equal(350);
      expect(instance.colWidth).to.equal(350);
      expect(instance.cols).to.equal(3);
      expect(instance.positions).to.deep.equal([600, 450, 450]);
    });

    it('can have a function for columns/gutters and span multiple columns', function () {
      fixture.style.width = '1200px';

      for (var i = 0; i < fixture.children.length; i++) {
        fixture.children[i].style.width = '300px';
        fixture.children[i].style.height = '10px';
      }

      fixture.children[1].style.width = '600px';
      fixture.children[5].style.width = '600px';
      fixture.children[6].style.width = '900px';

      instance = new Shuffle(fixture, {
        columnWidth: function (containerWidth) {
          expect(containerWidth).to.equal(1200);
          return 300;
        },

        gutterWidth: function () {
          return 0;
        },
      });

      expect(instance._getGutterSize(1200)).to.equal(0);
      expect(instance._getColumnSize(1200, 0)).to.equal(300);
      expect(instance.colWidth).to.equal(300);
      expect(instance.cols).to.equal(4);
      expect(instance.positions).to.deep.equal([40, 40, 30, 30]);
    });

    it('can filter by the data attribute', function (done) {
      instance = new Shuffle(fixture, {
        speed: 0,
      });

      function second() {
        expect(instance.visibleItems).to.equal(3);
        var hidden = [3, 4, 5, 6, 7, 8, 10].map(function (num) {
          return id('item' + num);
        });

        var visible = [1, 2, 9].map(function (num) {
          return id('item' + num);
        });

        hidden.forEach(function (element) {
          expect(element).to.have.class(Shuffle.Classes.HIDDEN);
          expect(element.style.visibility).to.equal('hidden');
        });

        visible.forEach(function (element) {
          expect(element).to.have.class(Shuffle.Classes.VISIBLE);
          expect(element.style.visibility).to.equal('visible');
        });

        once(fixture, Shuffle.EventType.LAYOUT, third);

        // Filter by green.
        instance.filter('green');
      }

      function third() {
        expect(instance.visibleItems).to.equal(2);
        var hidden = [1, 2, 5, 6, 7, 8, 9, 10].map(function (num) {
          return id('item' + num);
        });

        var visible = [3, 4].map(function (num) {
          return id('item' + num);
        });

        hidden.forEach(function (element) {
          expect(element).to.have.class(Shuffle.Classes.HIDDEN);
          expect(element.style.visibility).to.equal('hidden');
        });

        visible.forEach(function (element) {
          expect(element).to.have.class(Shuffle.Classes.VISIBLE);
          expect(element.style.visibility).to.equal('visible');
        });

        done();
      }

      once(fixture, Shuffle.EventType.LAYOUT, second);
      instance.filter('design');
    });

    it('can initialize filtered and the category parameter is optional', function () {
      instance = new Shuffle(fixture, {
        speed: 40,
        group: 'design',
      });

      expect(instance.visibleItems).to.equal(3);
    });

    it('can initialize sorted', function () {
      var sortObj = {
        by: function (element) {
          return parseInt(element.getAttribute('data-age'), 10);
        },
      };

      instance = new Shuffle(fixture, {
        speed: 40,
        initialSort: sortObj,
      });

      expect(instance.lastSort).to.deep.equal(sortObj);
    });

    it('can calculate column spans', function () {
      // itemWidth, columnWidth, columns, threshold
      expect(Shuffle.__getColumnSpan(50, 100, 3, 0)).to.equal(1);
      expect(Shuffle.__getColumnSpan(200, 100, 3, 0)).to.equal(2);
      expect(Shuffle.__getColumnSpan(200, 200, 3, 0)).to.equal(1);
      expect(Shuffle.__getColumnSpan(300, 100, 3, 0)).to.equal(3);

      // Column span should not be larger than the number of columns.
      expect(Shuffle.__getColumnSpan(300, 50, 3, 0)).to.equal(3);

      // Fix for percentage values.
      expect(Shuffle.__getColumnSpan(100.02, 100, 4, 0)).to.equal(2);
      expect(Shuffle.__getColumnSpan(100.02, 100, 4, 0.01)).to.equal(1);
      expect(Shuffle.__getColumnSpan(99.98, 100, 4, 0.01)).to.equal(1);
    });

    it('can calculate column sets', function () {
      // getAvailablePositions(positions, columnSpan, columns)
      var positions = [150, 0, 0, 0];
      expect(Shuffle.__getAvailablePositions(positions, 1, 4)).to.deep.equal([150, 0, 0, 0]);
      expect(Shuffle.__getAvailablePositions(positions, 2, 4)).to.deep.equal([150, 0, 0]);
    });

    it('can get an element option', function () {
      instance = new Shuffle(fixture);
      var first = fixture.firstElementChild;

      expect(instance._getElementOption(first)).to.equal(first);
      expect(instance._getElementOption('#item1')).to.equal(first);
      expect(instance._getElementOption('#hello-world')).to.be.null;
      expect(instance._getElementOption(null)).to.be.null;
      expect(instance._getElementOption(undefined)).to.be.null;
      expect(instance._getElementOption(function () {
        return first;
      })).to.be.null;
    });

    it('can test elements against filters', function () {
      instance = new Shuffle(fixture);

      var first = fixture.firstElementChild;
      expect(instance._doesPassFilter('design', first)).to.be.true;
      expect(instance._doesPassFilter('black', first)).to.be.false;

      expect(instance._doesPassFilter(function (element) {
        expect(element).to.exist;
        return element.getAttribute('data-age') === '21';
      }, first)).to.equal(true);

      expect(instance._doesPassFilter(function (element) {
        return element.getAttribute('data-age') === '22';
      }, first)).to.equal(false);
    });

    it('will maintain the last sort object', function () {
      instance = new Shuffle(fixture);
      var initialSort = instance.lastSort;

      instance.sort();
      expect(instance.lastSort).to.deep.equal(initialSort);

      instance.sort({ glen: true });
      expect(instance.lastSort).to.deep.equal({ glen: true });

      instance.sort();
      expect(instance.lastSort).to.deep.equal({ glen: true });

    });

    it('should reset columns', function () {
      instance = new Shuffle(fixture);

      expect(instance.cols).to.be.above(0);
      instance._resetCols();

      var positions = new Array(instance.cols);
      for (var i = 0; i < instance.cols; i++) {
        positions[i] = 0;
      }

      expect(instance.positions).to.deep.equal(positions);
    });

    it('should destroy properly', function () {
      instance = new Shuffle(fixture);
      instance.destroy();

      expect(instance.element).to.be.null;
      expect(instance.items).to.be.null;
      expect(instance.options.sizer).to.be.null;
      expect(instance.isDestroyed).to.be.true;

      expect(fixture).to.not.have.class('shuffle');

      toArray(fixture.children).forEach(function (child) {
        expect(child).to.not.have.class('shuffle-item');
        expect(child).to.not.have.class('shuffle-item--visible');
        expect(child).to.not.have.class('shuffle-item--hidden');
      });
    });

    it('should not update or shuffle when disabled or destroyed', function () {
      instance = new Shuffle(fixture);
      var update = sinon.spy(instance, 'update');
      var _filter = sinon.spy(instance, '_filter');

      instance.disable();

      instance.filter('design');

      expect(_filter.called).to.be.false;
      expect(update.called).to.be.false;

      instance.enable(false);

      instance.destroy();
      instance._onResize();
      expect(update.called).to.be.false;
    });

    it('should not update when the container is the same size', function () {
      instance = new Shuffle(fixture);
      var update = sinon.spy(instance, 'update');

      instance._onResize();

      expect(update.called).to.be.false;
    });

    describe('removing elements', function () {
      var children;

      beforeEach(function () {
        children = toArray(fixture.children);
      });

      afterEach(function () {
        children = null;
      });

      it('can remove items', function (done) {
        instance = new Shuffle(fixture, {
          speed: 16,
        });

        once(fixture, Shuffle.EventType.REMOVED, function (evt) {
          var detail = evt.detail;
          expect(detail.shuffle.visibleItems).to.equal(8);
          expect(detail.collection[0].id).to.equal('item1');
          expect(detail.collection[1].id).to.equal('item2');
          expect(detail.shuffle.element.children).to.have.lengthOf(8);
          expect(instance.isTransitioning).to.be.false;
          done();
        });

        var itemsToRemove = children.slice(0, 2);
        instance.remove(itemsToRemove);
      });

      it('can remove items without transforms', function (done) {
        instance = new Shuffle(fixture, {
          speed: 100,
          useTransforms: false,
        });

        once(fixture, Shuffle.EventType.REMOVED, function (evt) {
          var detail = evt.detail;
          expect(detail.shuffle.visibleItems).to.equal(8);
          expect(detail.collection[0].id).to.equal('item2');
          expect(detail.collection[1].id).to.equal('item3');
          expect(detail.shuffle.element.children).to.have.lengthOf(8);
          expect(detail.shuffle.isTransitioning).to.be.false;
          done();
        });

        var itemsToRemove = children.slice(1, 3);
        instance.remove(itemsToRemove);
      });
    });

    it('can get the real width of an element which is scaled', function () {
      var div = document.createElement('div');
      div.style.cssText = 'width:100px;height:100px;';
      div.style.transform = 'translate(0px,0px) scale(0.5)';

      document.body.appendChild(div);

      expect(Shuffle.getSize(div, false).width).to.equal(100);
      expect(Shuffle.getSize(div, true).width).to.equal(100);

      expect(Shuffle.getSize(div, false).height).to.equal(100);
      expect(Shuffle.getSize(div, true).height).to.equal(100);

      div.style.marginLeft = '10px';
      div.style.marginRight = '20px';
      div.style.marginTop = '30px';
      div.style.marginBottom = '40px';

      expect(Shuffle.getSize(div, false).width).to.equal(100);
      expect(Shuffle.getSize(div, true).width).to.equal(130);

      expect(Shuffle.getSize(div, false).height).to.equal(100);
      expect(Shuffle.getSize(div, true).height).to.equal(170);

      document.body.removeChild(div);
    });

    describe('inserting elements', function () {
      var items = [];

      beforeEach(function () {
        var eleven = document.createElement('div');
        eleven.className = 'item';
        eleven.setAttribute('data-age', 36);
        eleven.setAttribute('data-groups', '["ux", "black"]');
        eleven.id = 'item11';
        eleven.textContent = 'Person 11';

        var twelve = document.createElement('div');
        twelve.className = 'item';
        twelve.setAttribute('data-age', 37);
        twelve.setAttribute('data-groups', '["strategy", "blue"]');
        twelve.id = 'item12';
        twelve.textContent = 'Person 12';

        items.push(eleven, twelve);

        instance = new Shuffle(fixture, {
          speed: 100,
          group: 'black',
        });
      });

      afterEach(function (done) {
        once(fixture, Shuffle.EventType.LAYOUT, function () {
          items.length = 0;
          done();
        });
      });

      it('can add items', function () {
        fixture.appendChild(items[0]);
        fixture.appendChild(items[1]);
        instance.add(items);

        // Already 2 in the items, plus number 11.
        expect(instance.visibleItems).to.equal(3);
        expect(instance.items).to.have.lengthOf(12);
      });

      it('can prepend items', function () {
        fixture.insertBefore(items[1], fixture.firstElementChild);
        fixture.insertBefore(items[0], fixture.firstElementChild);
        instance.add(items);

        expect(instance.items[0].element).to.equal(items[0]);
        expect(instance.items[1].element).to.equal(items[1]);
        expect(instance.items).to.have.lengthOf(12);
      });

    });
  });

  describe('the sorter', function () {
    var items;
    var clone;

    beforeEach(function (done) {
      appendFixture('regular').then(function () {
        items = toArray(fixture.children).map(function (element) {
          return { element: element };
        });

        clone = toArray(items);

        done();
      });
    });

    afterEach(function () {
      items.length = 0;
      clone.length = 0;
      removeFixture();
    });

    it('will catch empty objects', function () {
      expect(Shuffle.__sorter({})).to.deep.equal([]);
    });

    it('can randomize the elements', function () {
      expect(items).to.have.lengthOf(10);
      expect(Shuffle.__sorter(items)).to.deep.equal(items);

      var clone = toArray(items);
      expect(Shuffle.__sorter(clone, { randomize: true })).to.not.deep.equal(items);
    });

    it('can sort by a function', function () {
      clone.sort(function (a, b) {
        var age1 = parseInt(a.element.getAttribute('data-age'), 10);
        var age2 = parseInt(b.element.getAttribute('data-age'), 10);
        return age1 - age2;
      });

      var result = Shuffle.__sorter(items, {
        by: function (element) {
          expect(element).to.exist;
          expect(element.nodeType).to.equal(1);
          return parseInt(element.getAttribute('data-age'), 10);
        },
      });

      expect(result).to.deep.equal(clone);
    });

    it('can sort by a function and reverse it', function () {
      clone.sort(function (a, b) {
        var age1 = parseInt(a.element.getAttribute('data-age'), 10);
        var age2 = parseInt(b.element.getAttribute('data-age'), 10);
        return age1 - age2;
      }).reverse();

      var result = Shuffle.__sorter(items, {
        reverse: true,
        by: function (element) {
          return parseInt(element.getAttribute('data-age'), 10);
        },
      });

      expect(result).to.deep.equal(clone);
    });

    it('will revert to DOM order if the `by` function returns undefined', function () {
      var count = 0;
      expect(Shuffle.__sorter(items, {
        by: function () {
          count++;
          return count < 5 ? Math.random() : undefined;
        },
      })).to.deep.equal(clone);
    });

    it('can sort things to the top', function () {
      items = items.slice(0, 4);
      var final = [items[1], items[0], items[3], items[2]];
      expect(Shuffle.__sorter(items, {
        by: function (element) {
          var age = parseInt(element.getAttribute('data-age'), 10);
          if (age === 50) {
            return 'sortFirst';
          } else {
            return age;
          }
        },
      })).to.deep.equal(final);
    });

    it('can sort things to the bottom', function () {
      items = items.slice(0, 4);
      var final = [items[0], items[2], items[1], items[3]];
      expect(Shuffle.__sorter(items, {
        by: function (element) {
          var age = parseInt(element.getAttribute('data-age'), 10);
          if (age === 27) {
            return 'sortLast';
          } else {
            return age;
          }
        },
      })).to.deep.equal(final);
    });

  });
});
