var assert = require('chai').assert;
var sinon = require('sinon');
var Promise = require('bluebird');
var ko = require('knockout');

var dom = require('app/client/lib/dom');
var clientUtil = require('../clientUtil');
var G = require('app/client/lib/browserGlobals').get('DocumentFragment');
var utils = require('../../utils');

describe('dom', function() {

  clientUtil.setTmpMochaGlobals();

  describe("dom construction", function() {
    it("should create elements with the right tag name, class and ID", function() {
      var elem = dom('div', "Hello world");
      assert.equal(elem.tagName, "DIV");
      assert(!elem.className);
      assert(!elem.id);
      assert.equal(elem.textContent, "Hello world");

      elem = dom('span#foo.bar.baz', "Hello world");
      assert.equal(elem.tagName, "SPAN");
      assert.equal(elem.className, "bar baz");
      assert.equal(elem.id, "foo");
      assert.equal(elem.textContent, "Hello world");
    });

    it("should set attributes", function() {
      var elem = dom('a', { title: "foo", id: "bar" });
      assert.equal(elem.title, "foo");
      assert.equal(elem.id, "bar");
    });

    it("should set children", function() {
      var elem = dom('div',
                     "foo", dom('a#a'),
                     [dom('a#b'), "bar", dom('a#c')],
                     dom.frag(dom('a#d'), "baz", dom('a#e')));
      assert.equal(elem.childNodes.length, 8);
      assert.equal(elem.childNodes[0].data, "foo");
      assert.equal(elem.childNodes[1].id, "a");
      assert.equal(elem.childNodes[2].id, "b");
      assert.equal(elem.childNodes[3].data, "bar");
      assert.equal(elem.childNodes[4].id, "c");
      assert.equal(elem.childNodes[5].id, "d");
      assert.equal(elem.childNodes[6].data, "baz");
      assert.equal(elem.childNodes[7].id, "e");
    });

    it('should flatten nested arrays and arrays returned from functions', function() {
      var values = ['apple', 'orange', ['banana', 'mango']];
      var elem = dom('ul',
        values.map(function(value, index) {
          return dom('li', value);
        }),
        [
          dom('li', 'pear'),
          [
            dom('li', 'peach'),
            dom('li', 'cranberry'),
          ],
          dom('li', 'date')
        ]
      );

      assert.equal(elem.outerHTML, "<ul><li>apple</li><li>orange</li>" +
        "<li>bananamango</li><li>pear</li><li>peach</li><li>cranberry</li>" +
        "<li>date</li></ul>");

      elem = dom('ul',
        function(innerElem) {
          return [
            dom('li', 'plum'),
            dom('li', 'pomegranate')
          ];
        },
        function(innerElem) {
          return function(moreInnerElem) {
            return [
              dom('li', 'strawberry'),
              dom('li', 'blueberry')
            ];
          };
        }
      );
      assert.equal(elem.outerHTML, "<ul><li>plum</li><li>pomegranate</li>" +
        "<li>strawberry</li><li>blueberry</li></ul>");

    });

    it("should append append values returned from functions except undefined", function() {
      var elem = dom('div',
        function(divElem) {
          divElem.classList.add('yogurt');
          return dom('div', 'sneakers');
        },
        dom('span', 'melon')
      );

      assert.equal(elem.classList[0], 'yogurt',
        'function shold have applied new class to outer div');
      assert.equal(elem.childNodes.length, 2);
      assert.equal(elem.childNodes[0].innerHTML, "sneakers");
      assert.equal(elem.childNodes[1].innerHTML, "melon");

      elem = dom('div',
        function(divElem) {
          return undefined;
        }
      );
      assert.equal(elem.childNodes.length, 0,
        "undefined returned from a function should not be added to the DOM tree");
    });

    it('should not append nulls', function() {
      var elem = dom('div',
        [
          "hello",
          null,
          "world",
          null,
          "jazz"
        ],
        'hands',
        null
      );
      assert.equal(elem.childNodes.length, 4,
        "undefined returned from a function should not be added to the DOM tree");
      assert.equal(elem.childNodes[0].data, "hello");
      assert.equal(elem.childNodes[1].data, "world");
      assert.equal(elem.childNodes[2].data, "jazz");
      assert.equal(elem.childNodes[3].data, "hands");
    });

  });

  utils.timing.describe("dom", function() {
    var built, child;
    before(function() {
      child = dom('bar');
    });
    utils.timing.it(40, "should be fast", function() {
      built = utils.repeat(100, function() {
        return dom('div#id1.class1.class2', {disabled: 'disabled'},
          'foo',
          child,
          ['hello', 'world'],
          function(elem) {
            return 'test';
          }
        );
      });
    });
    utils.timing.it(40, "should be fast", function() {
      utils.repeat(100, function() {
        dom('div#id1.class1.class2.class3');
        dom('div#id1.class1.class2.class3');
        dom('div#id1.class1.class2.class3');
        dom('div#id1.class1.class2.class3');
        dom('div#id1.class1.class2.class3');
      });
    });
    after(function() {
      assert.equal(built.getAttribute('disabled'), 'disabled');
      assert.equal(built.tagName, 'DIV');
      assert.equal(built.className, 'class1 class2');
      assert.equal(built.childNodes.length, 5);
      assert.equal(built.childNodes[0].data, 'foo');
      assert.equal(built.childNodes[1], child);
      assert.equal(built.childNodes[2].data, 'hello');
      assert.equal(built.childNodes[3].data, 'world');
      assert.equal(built.childNodes[4].data, 'test');
    });
  });

  describe("dom.frag", function() {
    it("should create DocumentFragments", function() {
      var elem1 = dom.frag(["hello", "world"]);
      assert(elem1 instanceof G.DocumentFragment);
      assert.equal(elem1.childNodes.length, 2);
      assert.equal(elem1.childNodes[0].data, "hello");
      assert.equal(elem1.childNodes[1].data, "world");

      var elem2 = dom.frag("hello", "world");
      assert(elem2 instanceof G.DocumentFragment);
      assert.equal(elem2.childNodes.length, 2);
      assert.equal(elem2.childNodes[0].data, "hello");
      assert.equal(elem2.childNodes[1].data, "world");

      var elem3 = dom.frag(dom("div"), [dom("span"), "hello"], "world");
      assert.equal(elem3.childNodes.length, 4);
      assert.equal(elem3.childNodes[0].tagName, "DIV");
      assert.equal(elem3.childNodes[1].tagName, "SPAN");
      assert.equal(elem3.childNodes[2].data, "hello");
      assert.equal(elem3.childNodes[3].data, "world");
    });
  });

  describe("inlineable", function() {
    it("should return a function suitable for use as dom argument", function() {
      var ctx = {a:1}, b = dom('span'), c = {c:1};
      var spy = sinon.stub().returns(c);
      var inlinable = dom.inlineable(spy);

      // When the first argument is a Node, then calling inlineable is the same as calling spy.
      inlinable.call(ctx, b, c, 1, "asdf");
      sinon.assert.calledOnce(spy);
      sinon.assert.calledOn(spy, ctx);
      sinon.assert.calledWithExactly(spy, b, c, 1, "asdf");
      assert.strictEqual(spy.returnValues[0], c);
      spy.reset();
      spy.returns(c);

      // When the first Node argument is omitted, then the call is deferred. Check that it works
      // correctly.
      var func = inlinable.call(ctx, c, 1, "asdf");
      sinon.assert.notCalled(spy);
      assert.equal(typeof func, 'function');
      assert.deepEqual(spy.returnValues, []);
      let r = func(b);
      sinon.assert.calledOnce(spy);
      sinon.assert.calledOn(spy, ctx);
      sinon.assert.calledWithExactly(spy, b, c, 1, "asdf");
      assert.deepEqual(r, c);
      assert.strictEqual(spy.returnValues[0], c);
    });
  });

  utils.timing.describe("dom.inlinable", function() {
    var elem, spy, inlinableCounter, inlinableSpy, count = 0;
    before(function() {
      elem = dom('span');
      spy = sinon.stub();
      inlinableCounter = dom.inlinable(function(elem, a, b) {
        count++;
      });
      inlinableSpy = dom.inlinable(spy);
    });

    utils.timing.it(25, "should be fast", function() {
      utils.repeat(10000, function() {
        inlinableCounter(1, "asdf")(elem);
        inlinableCounter(1, "asdf")(elem);
        inlinableCounter(1, "asdf")(elem);
        inlinableCounter(1, "asdf")(elem);
        inlinableCounter(1, "asdf")(elem);
      });
      inlinableSpy()(elem);
      inlinableSpy(1)(elem);
      inlinableSpy(1, "asdf")(elem);
      inlinableSpy(1, "asdf", 56)(elem);
      inlinableSpy(1, "asdf", 56, "hello")(elem);
    });

    after(function() {
      assert.equal(count, 50000);
      sinon.assert.callCount(spy, 5);
      assert.deepEqual(spy.args[0], [elem]);
      assert.deepEqual(spy.args[1], [elem, 1]);
      assert.deepEqual(spy.args[2], [elem, 1, "asdf"]);
      assert.deepEqual(spy.args[3], [elem, 1, "asdf", 56]);
      assert.deepEqual(spy.args[4], [elem, 1, "asdf", 56, "hello"]);
    });
  });

  describe("dom.defer", function() {
    it("should call supplied function after the current call stack", function() {
      var obj = {};
      var spy1 = sinon.spy();
      var spy2 = sinon.spy();
      var div, span;
      dom('div',
        span = dom('span', dom.defer(spy1, obj)),
        div = dom('div', spy2)
      );
      sinon.assert.calledOnce(spy2);
      sinon.assert.calledWithExactly(spy2, div);
      sinon.assert.notCalled(spy1);
      return Promise.delay(0).then(function() {
        sinon.assert.calledOnce(spy2);
        sinon.assert.calledOnce(spy1);
        assert(spy2.calledBefore(spy1));
        sinon.assert.calledOn(spy1, obj);
        sinon.assert.calledWithExactly(spy1, span);
      });
    });
  });

  describe("dom.onDispose", function() {
    it("should call supplied function when an element is cleaned up", function() {
      var obj = {};
      var spy1 = sinon.spy();
      var spy2 = sinon.spy();
      var div, span;
      div = dom('div',
        span = dom('span', dom.onDispose(spy1, obj)),
        dom.onDispose(spy2)
      );
      sinon.assert.notCalled(spy1);
      sinon.assert.notCalled(spy2);
      ko.virtualElements.emptyNode(div);
      sinon.assert.notCalled(spy2);
      sinon.assert.calledOnce(spy1);
      sinon.assert.calledOn(spy1, obj);
      sinon.assert.calledWithExactly(spy1, span);
      ko.removeNode(div);
      sinon.assert.calledOnce(spy1);
      sinon.assert.calledOnce(spy2);
      sinon.assert.calledOn(spy2, undefined);
      sinon.assert.calledWithExactly(spy2, div);
    });
  });

  describe("dom.autoDispose", function() {
    it("should call dispose the supplied value when an element is cleaned up", function() {
      var obj = { dispose: sinon.spy() };
      var div = dom('div', dom.autoDispose(obj));
      ko.cleanNode(div);
      sinon.assert.calledOnce(obj.dispose);
      sinon.assert.calledOn(obj.dispose, obj);
      sinon.assert.calledWithExactly(obj.dispose);
    });
  });

  describe("dom.findLastChild", function() {
    it("should return last matching child", function() {
      var el = dom('div', dom('div.a.b'), dom('div.b.c'), dom('div.c.d'));
      assert.equal(dom.findLastChild(el, '.b').className, 'b c');
      assert.equal(dom.findLastChild(el, '.f'), null);
      assert.equal(dom.findLastChild(el, '.c.d').className, 'c d');
      assert.equal(dom.findLastChild(el, '.b.a').className, 'a b');
      function filter(elem) { return elem.classList.length === 2; }
      assert.equal(dom.findLastChild(el, filter).className, 'c d');
    });
  });

  describe("dom.findAncestor", function() {
    var el1, el2, el3, el4;
    before(function() {
      el1 = dom('div.foo.bar',
        el2 = dom('div.foo',
          el3 = dom('div.baz')
        ),
        el4 = dom('div.foo.bar2')
      );
    });

    function assertSameElem(elem1, elem2) {
      assert(elem1 === elem2, "Expected " + elem1 + " to be " + elem2);
    }

    it("should return the child itself if it matches", function() {
      assertSameElem(dom.findAncestor(el3, null, '.baz'), el3);
      assertSameElem(dom.findAncestor(el3, el3, '.baz'), el3);
    });

    it("should stop at the nearest match", function() {
      assertSameElem(dom.findAncestor(el3, null, '.foo'), el2);
      assertSameElem(dom.findAncestor(el3, el1, '.foo'), el2);
      assertSameElem(dom.findAncestor(el3, el2, '.foo'), el2);
      assertSameElem(dom.findAncestor(el3, el3, '.foo'), null);
    });

    it("should not go past container", function() {
      assertSameElem(dom.findAncestor(el3, null, '.bar'), el1);
      assertSameElem(dom.findAncestor(el3, el1, '.bar'), el1);
      assertSameElem(dom.findAncestor(el3, el2, '.bar'), null);
      assertSameElem(dom.findAncestor(el3, el3, '.bar'), null);
    });

    it("should fail if child is outside of container", function() {
      assertSameElem(dom.findAncestor(el3, el4, '.foo'), null);
      assertSameElem(dom.findAncestor(el2, el3, '.foo'), null);
    });

    it("should return null for no matches", function() {
      assertSameElem(dom.findAncestor(el3, null, '.blah'), null);
      assertSameElem(dom.findAncestor(el3, el1, '.blah'), null);
      assertSameElem(dom.findAncestor(el3, el2, '.blah'), null);
      assertSameElem(dom.findAncestor(el3, el3, '.blah'), null);
    });

    function filter(elem) { return elem.classList.length === 2; }
    it("should handle a custom filter function", function() {
      assertSameElem(dom.findAncestor(el3, null, filter), el1);
      assertSameElem(dom.findAncestor(el3, el1, filter), el1);
      assertSameElem(dom.findAncestor(el3, el2, filter), null);
      assertSameElem(dom.findAncestor(el3, el3, filter), null);
      assertSameElem(dom.findAncestor(el3, el4, filter), null);
    });
  });
});