/**
 * loadjs tests
 * @module test/tests.js
 */

var pathsLoaded = null,  // file register
  testEl = null,
  assert = chai.assert,
  expect = chai.expect;


describe('LoadJS tests', function () {

  beforeEach(function () {
    // reset register
    pathsLoaded = {};

    // reset loadjs dependencies
    loadjs.reset();
  });

  // ==========================================================================
  // JavaScript file loading tests
  // ==========================================================================

  describe('JavaScript file loading tests', function () {

    it('should call success callback on valid path', function (done) {
      loadjs(['assets/file1.js'], {
        success: function () {
          assert.equal(pathsLoaded['file1.js'], true);
          done();
        }
      });
    });


    it('should call error callback on invalid path', function (done) {
      loadjs(['assets/file-doesntexist.js'], {
        success: function () {
          throw "Executed success callback";
        },
        error: function (pathsNotFound) {
          assert.equal(pathsNotFound.length, 1);
          assert.equal(pathsNotFound[0], 'assets/file-doesntexist.js');
          done();
        }
      });
    });


    it('should call before callback before embedding into document', function (done) {
      var scriptTags = [];

      loadjs(['assets/file1.js', 'assets/file2.js'], {
        before: function (path, el) {
          scriptTags.push({
            path: path,
            el: el
          });

          // add cross origin script for file2
          if (path === 'assets/file2.js') {
            el.crossOrigin = 'anonymous';
          }
        },
        success: function () {
          assert.equal(scriptTags[0].path, 'assets/file1.js');
          assert.equal(scriptTags[1].path, 'assets/file2.js');

          assert.equal(scriptTags[0].el.crossOrigin, undefined);
          assert.equal(scriptTags[1].el.crossOrigin, 'anonymous');

          done();
        }
      });
    });


    it('should bypass insertion if before returns `false`', function (done) {
      loadjs(['assets/file1.js'], {
        before: function (path, el) {
          // append to body (instead of head)
          document.body.appendChild(el);

          // return `false` to bypass default DOM insertion
          return false;
        },
        success: function () {
          assert.equal(pathsLoaded['file1.js'], true);

          // verify that file was added to body
          var els = document.body.querySelectorAll('script'),
            el;

          for (var i = 0; i < els.length; i++) {
            el = els[i];
            if (el.src.indexOf('assets/file1.js') !== -1) done();
          }
        }
      });
    });


    it('should call success callback on two valid paths', function (done) {
      loadjs(['assets/file1.js', 'assets/file2.js'], {
        success: function () {
          assert.equal(pathsLoaded['file1.js'], true);
          assert.equal(pathsLoaded['file2.js'], true);
          done();
        }
      });
    });


    it('should call error callback on one invalid path', function (done) {
      loadjs(['assets/file1.js', 'assets/file-doesntexist.js'], {
        success: function () {
          throw "Executed success callback";
        },
        error: function (pathsNotFound) {
          assert.equal(pathsLoaded['file1.js'], true);
          assert.equal(pathsNotFound.length, 1);
          assert.equal(pathsNotFound[0], 'assets/file-doesntexist.js');
          done();
        }
      });
    });


    it('should support async false', function (done) {
      this.timeout(5000);

      var numCompleted = 0,
        numTests = 20,
        paths = ['assets/asyncfalse1.js', 'assets/asyncfalse2.js'];

      // run tests sequentially
      var testFn = function (paths) {
        // add cache busters
        var pathsUncached = paths.slice(0);
        pathsUncached[0] += '?_=' + Math.random();
        pathsUncached[1] += '?_=' + Math.random();

        loadjs(pathsUncached, {
          success: function () {
            var f1 = paths[0].replace('assets/', '');
            var f2 = paths[1].replace('assets/', '');

            // check load order
            assert.isTrue(pathsLoaded[f1]);
            assert.isFalse(pathsLoaded[f2]);

            // increment tests
            numCompleted += 1;

            if (numCompleted === numTests) {
              // exit
              done();
            } else {
              // reset register
              pathsLoaded = {};

              // run test again
              paths.reverse();
              testFn(paths);
            }
          },
          async: false
        });
      };

      // run tests
      testFn(paths);
    });


    it('should support multiple tries', function (done) {
      loadjs('assets/file-numretries.js', {
        error: function () {
          // check number of scripts in document
          var selector = 'script[src="assets/file-numretries.js"]',
            scripts = document.querySelectorAll(selector);
          if (scripts.length === 2) done();
        },
        numRetries: 1
      });
    });

    // tests for browsers with/without module support
    if (caniuseModule()) {
      describe('module tests for browsers with module support', function () {

        it('should support loading modules with "module!" modifier', function (done) {
          loadjs(['module!assets/module1.js'], {
            success: function () {
              assert.equal(pathsLoaded['module1.js'], true);
              done();
            }
          });
        });


        it('should call error callback on invalid module! path', function (done) {
          loadjs(['module!assets/module-doesntexist.js'], {
            success: function () {
              throw "Executed success callback";
            },
            error: function (pathsNotFound) {
              assert.equal(pathsNotFound.length, 1);
              assert.equal(pathsNotFound[0], 'module!assets/module-doesntexist.js');
              done();
            }
          });
        });


        it('should support bypassing loading files with "nomodule!" modifier', function (done) {
          loadjs(['nomodule!assets/file1.js'], {
            success: function () {
              assert.equal(pathsLoaded['file1.js'], undefined);
              done();
            }
          });
        });
      });
    } else {
      describe('module tests for browsers without module support', function () {

        it('should support loading modules with "nomodule!" modifier', function (done) {
          loadjs(['nomodule!assets/file1.js'], {
            success: function () {
              assert.equal(pathsLoaded['file1.js'], true);
              done();
            }
          });
        });


        it('should call error callback on invalid nomodule! path', function (done) {
          loadjs(['nomodule!assets/file-doesntexist.js'], {
            success: function () {
              throw "Executed success callback";
            },
            error: function (pathsNotFound) {
              assert.equal(pathsNotFound.length, 1);
              assert.equal(pathsNotFound[0], 'nomodule!assets/file-doesntexist.js');
              done();
            }
          });
        });


        it('should support bypassing loading files with "module!" modifier', function (done) {
          loadjs(['module!assets/module1.js'], {
            success: function () {
              assert.equal(pathsLoaded['module1.js'], undefined);
              done();
            }
          });
        });
      });
    }


    // Un-'x' this for testing ad blocked scripts.
    //   Ghostery: Disallow "Google Adservices"
    //   AdBlock Plus: Add "www.googletagservices.com/tag/js/gpt.js" as a
    //   custom filter under Options
    //
    xit('it should report ad blocked scripts as missing', function (done) {
      var s1 = 'https://www.googletagservices.com/tag/js/gpt.js',
        s2 = 'https://munchkin.marketo.net/munchkin-beta.js';

      loadjs([s1, s2, 'assets/file1.js'], {
        success: function () {
          throw new Error('Executed success callback');
        },
        error: function (pathsNotFound) {
          assert.equal(pathsLoaded['file1.js'], true);
          assert.equal(pathsNotFound.length, 2);
          assert.equal(pathsNotFound[0], s1);
          assert.equal(pathsNotFound[1], s2);
          done();
        }
      });
    });
  });


  // ==========================================================================
  // CSS file loading tests
  // ==========================================================================

  describe('CSS file loading tests', function () {

    before(function () {
      // add test div to body for css tests
      testEl = document.createElement('div');
      testEl.className = 'test-div mui-container';
      testEl.style.display = 'inline-block';
      document.body.appendChild(testEl);
    });


    afterEach(function () {
      var els = document.getElementsByTagName('link'),
        i = els.length,
        el;

      // iteratete through stylesheets
      while (i--) {
        el = els[i];

        // remove test stylesheets
        if (el.href.indexOf('mocha.css') === -1) {
          el.parentNode.removeChild(el);
        }
      }
    });


    it('should load one file', function (done) {
      loadjs(['assets/file1.css'], {
        success: function () {
          assert.equal(testEl.offsetWidth, 100);
          done();
        }
      });
    });


    it('should load multiple files', function (done) {
      loadjs(['assets/file1.css', 'assets/file2.css'], {
        success: function () {
          assert.equal(testEl.offsetWidth, 200);
          done();
        }
      });
    });


    it('should call error callback on one invalid path', function (done) {
      loadjs(['assets/file1.css', 'assets/file-doesntexist.css'], {
        success: function () {
          throw new Error('Executed success callback');
        },
        error: function (pathsNotFound) {
          assert.equal(testEl.offsetWidth, 100);
          assert.equal(pathsNotFound.length, 1);
          assert.equal(pathsNotFound[0], 'assets/file-doesntexist.css');
          done();
        }
      });
    });


    it('should support mix of css and js', function (done) {
      loadjs(['assets/file1.css', 'assets/file1.js'], {
        success: function () {
          assert.equal(pathsLoaded['file1.js'], true);
          assert.equal(testEl.offsetWidth, 100);
          done();
        }
      });
    });


    it('should support forced "css!" files', function (done) {
      loadjs(['css!assets/file1.css'], {
        success: function () {
          // loop through files
          var els = document.getElementsByTagName('link'),
            i = els.length,
            el;

          while (i--) {
            if (els[i].href.indexOf('file1.css') !== -1) done();
          }
        }
      });
    });


    it('supports urls with query arguments', function (done) {
      loadjs(['assets/file1.css?x=x'], {
        success: function () {
          assert.equal(testEl.offsetWidth, 100);
          done();
        }
      });
    });


    it('supports urls with anchor tags', function (done) {
      loadjs(['assets/file1.css#anchortag'], {
        success: function () {
          assert.equal(testEl.offsetWidth, 100);
          done();
        }
      });
    });


    it('supports urls with query arguments and anchor tags', function (done) {
      loadjs(['assets/file1.css?x=x#anchortag'], {
        success: function () {
          assert.equal(testEl.offsetWidth, 100);
          done();
        }
      });
    });


    it('should load external css files', function (done) {
      this.timeout(0);

      loadjs(['//cdn.muicss.com/mui-0.6.8/css/mui.min.css'], {
        success: function () {
          var styleObj = getComputedStyle(testEl);

          assert.equal(styleObj.getPropertyValue('padding-left'), '15px');
          done();
        }
      });
    });


    it('should call error on missing external file', function (done) {
      this.timeout(0);

      loadjs(['//cdn.muicss.com/mui-0.6.8/css/mui-doesnotexist.min.css'], {
        success: function () {
          throw new Error('Executed success callback');
        },
        error: function (pathsNotFound) {
          var styleObj = getComputedStyle(testEl);

          assert.equal(styleObj.getPropertyValue('padding-left'), '0px');
          assert.equal(pathsNotFound.length, 1);
          done();
        }
      });
    });

    // teardown
    return after(function () {
      // remove test div
      testEl.parentNode.removeChild(testEl);
    });
  });

  // ==========================================================================
  // Image file loading tests
  // ==========================================================================

  describe('Image file loading tests', function () {

    function assertLoaded(src) {
      // loop through images
      var imgs = document.getElementsByTagName('img');

      Array.prototype.slice.call(imgs).forEach(function (img) {
        // verify image was loaded
        if (img.src === src) assert.equal(img.naturalWidth > 0, true);
      });
    }


    function assertNotLoaded(src) {
      // loop through images
      var imgs = document.getElementsByTagName('img');

      Array.prototype.slice.call(imgs).forEach(function (img) {
        // fail if image was loaded
        if (img.src === src) assert.equal(img.naturalWidth, 0);
      });
    }


    it('should load one file', function (done) {
      loadjs(['assets/flash.png'], {
        success: function () {
          assertLoaded('assets/flash.png');
          done();
        }
      });
    });


    it('should load multiple files', function (done) {
      loadjs(['assets/flash.png', 'assets/flash.jpg'], {
        success: function () {
          assertLoaded('assets/flash.png');
          assertLoaded('assets/flash.jpg');
          done();
        }
      });
    });


    it('detects png|gif|jpg|svg extensions', function (done) {
      let files = [
        'assets/flash.png',
        'assets/flash.gif',
        'assets/flash.jpg',
        'assets/flash.svg'
      ];

      loadjs(files, function () {
        files.forEach(function (file) {
          assertLoaded(file);
        });
        done();
      });
    });


    it('supports urls with query arguments', function (done) {
      var src = 'assets/flash.png?' + Math.random();

      loadjs([src], {
        success: function () {
          assertLoaded(src);
          done();
        }
      });
    });


    it('supports urls with anchor tags', function (done) {
      var src = 'assets/flash.png#' + Math.random();

      loadjs([src], {
        success: function () {
          assertLoaded(src);
          done();
        }
      });
    });


    it('supports urls with query arguments and anchor tags', function (done) {
      var src = 'assets/flash.png';
      src += '?' + Math.random();
      src += '#' + Math.random();

      loadjs([src], {
        success: function () {
          assertLoaded(src);
          done();
        }
      });
    });


    it('should support forced "img!" files', function (done) {
      var src = 'assets/flash.png?' + Math.random();

      loadjs(['img!' + src], {
        success: function () {
          assertLoaded(src);
          done();
        }
      });
    });


    it('should call error callback on one invalid path', function (done) {
      var src1 = 'assets/flash.png?' + Math.random(),
        src2 = 'assets/flash-doesntexist.png?' + Math.random();

      loadjs(['img!' + src1, 'img!' + src2], {
        success: function () {
          throw new Error('Executed success callback');
        },
        error: function (pathsNotFound) {
          assert.equal(pathsNotFound.length, 1);
          assertLoaded(src1);
          assertNotLoaded(src2);
          done();
        }
      });
    });


    it('should support mix of img and js', function (done) {
      var src = 'assets/flash.png?' + Math.random();

      loadjs(['img!' + src, 'assets/file1.js'], {
        success: function () {
          assert.equal(pathsLoaded['file1.js'], true);
          assertLoaded(src);
          done();
        }
      });
    });


    it('should load external img files', function (done) {
      this.timeout(0);

      var src = 'https://www.muicss.com/static/images/mui-logo.png?';
      src += Math.random();

      loadjs(['img!' + src], {
        success: function () {
          assertLoaded(src);
          done();
        }
      });
    });


    it('should call error on missing external file', function (done) {
      this.timeout(0);

      var src = 'https://www.muicss.com/static/images/';
      src += 'mui-logo-doesntexist.png?' + Math.random();

      loadjs(['img!' + src], {
        success: function () {
          throw new Error('Executed success callback');
        },
        error: function (pathsNotFound) {
          assertNotLoaded(src);
          done();
        }
      });
    });


    if (caniuseWebp()) {
      describe('tests for browsers with webp support', function () {

        it('detects webp extensions', function (done) {
          loadjs('assets/flash.webp', function () {
            assertLoaded('assets/flash.webp');
            done();
          });
        });

      });
    } else {
      describe('tests for browsers without webp support', function () {

        it('executes error callback when browser loads webp file', function (done) {
          loadjs('assets/flash.webp', {
            error: function (pathsNotFound) {
              assertNotLoaded('assets/flash.webp');
              done();
            }
          });
        });

      });
    }
  });


  // ==========================================================================
  // API tests
  // ==========================================================================

  describe('API tests', function () {

    it('should throw an error if bundle is already defined', function () {
      // define bundle
      loadjs(['assets/file1.js'], 'bundle');

      // define bundle again
      var fn = function () {
        loadjs(['assets/file1.js'], 'bundle');
      };

      expect(fn).to.throw("LoadJS");
    });


    it('should create a bundle id and a callback inline', function (done) {
      loadjs(['assets/file1.js', 'assets/file2.js'], 'bundle', {
        success: function () {
          assert.equal(pathsLoaded['file1.js'], true);
          assert.equal(pathsLoaded['file2.js'], true);
          done();
        }
      });
    });


    it('should chain loadjs object', function (done) {
      function bothDone() {
        if (pathsLoaded['file1.js'] && pathsLoaded['file2.js']) done();
      }

      // define bundles
      loadjs('assets/file1.js', 'bundle1');
      loadjs('assets/file2.js', 'bundle2');

      loadjs
        .ready('bundle1', {
          success: function () {
            assert.equal(pathsLoaded['file1.js'], true);
            bothDone();
          }
        })
        .ready('bundle2', {
          success: function () {
            assert.equal(pathsLoaded['file2.js'], true);
            bothDone();
          }
        });
    });


    it('should handle multiple dependencies', function (done) {
      loadjs('assets/file1.js', 'bundle1');
      loadjs('assets/file2.js', 'bundle2');

      loadjs.ready(['bundle1', 'bundle2'], {
        success: function () {
          assert.equal(pathsLoaded['file1.js'], true);
          assert.equal(pathsLoaded['file2.js'], true);
          done();
        }
      });
    });


    it('should error on missing depdendencies', function (done) {
      loadjs('assets/file1.js', 'bundle1');
      loadjs('assets/file-doesntexist.js', 'bundle2');

      loadjs.ready(['bundle1', 'bundle2'], {
        success: function () {
          throw "Executed success callback";
        },
        error: function (depsNotFound) {
          assert.equal(pathsLoaded['file1.js'], true);
          assert.equal(depsNotFound.length, 1);
          assert.equal(depsNotFound[0], 'bundle2');
          done();
        }
      });
    });


    it('should execute callbacks on .done()', function (done) {
      // add handler
      loadjs.ready('plugin', {
        success: function () {
          done();
        }
      });

      // execute done
      loadjs.done('plugin');
    });


    it('should execute callbacks created after .done()', function (done) {
      // execute done
      loadjs.done('plugin');

      // add handler
      loadjs.ready('plugin', {
        success: function () {
          done();
        }
      });
    });


    it('should define bundles', function (done) {
      // define bundle
      loadjs(['assets/file1.js', 'assets/file2.js'], 'bundle');

      // use 1 second delay to let files load
      setTimeout(function () {
        loadjs.ready('bundle', {
          success: function () {
            assert.equal(pathsLoaded['file1.js'], true);
            assert.equal(pathsLoaded['file2.js'], true);
            done();
          }
        });
      }, 1000);
    });


    it('should allow bundle callbacks before definitions', function (done) {
      // define callback
      loadjs.ready('bundle', {
        success: function () {
          assert.equal(pathsLoaded['file1.js'], true);
          assert.equal(pathsLoaded['file2.js'], true);
          done();
        }
      });

      // use 1 second delay
      setTimeout(function () {
        loadjs(['assets/file1.js', 'assets/file2.js'], 'bundle');
      }, 1000);
    });


    it('should reset dependencies statuses', function () {
      loadjs(['assets/file1.js'], 'cleared');
      loadjs.reset();

      // define bundle again
      var fn = function () {
        loadjs(['assets/file1.js'], 'cleared');
      };

      expect(fn).not.to.throw("LoadJS");
    });


    it('should indicate if bundle has already been defined', function () {
      loadjs(['assets/file1/js'], 'bundle1');

      assert.equal(loadjs.isDefined('bundle1'), true);
      assert.equal(loadjs.isDefined('bundleXX'), false);
    });


    it('should accept success callback functions to loadjs()', function (done) {
      loadjs('assets/file1.js', function () {
        done();
      });
    });


    it('should accept success callback functions to .ready()', function (done) {
      loadjs.done('plugin');
      loadjs.ready('plugin', function () {
        done();
      });
    });


    if (caniusePromise()) {
      describe('tests for browsers with Promise support', function () {

        it('should return Promise object if returnPromise is true', function () {
          var prom = loadjs(['assets/file1.js'], { returnPromise: true });

          // verify that response object is a Promise
          assert.equal(prom instanceof Promise, true);
        });


        it('Promise object should support resolutions', function (done) {
          var prom = loadjs(['assets/file1.js'], { returnPromise: true });

          prom.then(function () {
            assert.equal(pathsLoaded['file1.js'], true);
            done();
          });
        });


        it('Promise object should support rejections', function (done) {
          var prom = loadjs(['assets/file-doesntexist.js'], { returnPromise: true });

          prom.then(
            function () { },
            function (pathsNotFound) {
              assert.equal(pathsNotFound.length, 1);
              assert.equal(pathsNotFound[0], 'assets/file-doesntexist.js');
              done();
            }
          );
        });


        it('Promise object should support catches', function (done) {
          var prom = loadjs(['assets/file-doesntexist.js'], { returnPromise: true });

          prom
            .catch(function (pathsNotFound) {
              assert.equal(pathsNotFound.length, 1);
              assert.equal(pathsNotFound[0], 'assets/file-doesntexist.js');
              done();
            });
        });


        it('supports Promises and success callbacks', function (done) {
          var numCompleted = 0;

          function completedFn() {
            numCompleted += 1;
            if (numCompleted === 2) done();
          };

          var prom = loadjs('assets/file1.js', {
            success: completedFn,
            returnPromise: true
          });

          prom.then(completedFn);
        });


        it('supports Promises and bundle ready events', function (done) {
          var numCompleted = 0;

          function completedFn() {
            numCompleted += 1;
            if (numCompleted === 2) done();
          };

          loadjs('assets/file1.js', 'bundle1', { returnPromise: true })
            .then(completedFn);

          loadjs.ready('bundle1', completedFn);
        });

      });
    }
  });
});


// ==========================================================================
// Utilities
// ==========================================================================

// https://stackoverflow.com/questions/5573096/detecting-webp-support
function caniuseWebp() {
  var elem = document.createElement('canvas');
  if (!!(elem.getContext && elem.getContext('2d'))) {
    return elem.toDataURL('image/webp').indexOf('data:image/webp') == 0;
  } else {
    return false;
  }
}

function caniuseModule() {
  return 'noModule' in document.createElement('script');
}

function caniusePromise() {
  return typeof Promise !== "undefined";
}
