'use strict';

beforeEach(function() {

  function cssMatcher(presentClasses, absentClasses) {
    return function() {
      return {
        compare: function(actual) {
          var element = angular.element(actual);
          var present = true;
          var absent = false;

          angular.forEach(presentClasses.split(' '), function(className) {
            present = present && element.hasClass(className);
          });

          angular.forEach(absentClasses.split(' '), function(className) {
            absent = absent || element.hasClass(className);
          });

          var message = function() {
            return 'Expected to have ' + presentClasses +
              (absentClasses ? (' and not have ' + absentClasses + '') : '') +
              ' but had ' + element[0].className + '.';
          };
          return {
            pass: present && !absent,
            message: message
          };
        }
      };
    };
  }

  function DOMTester(a, b) {
    if (a && b && a.nodeType > 0 && b.nodeType > 0) {
      return a === b;
    }
  }

  function isNgElementHidden(element) {
    // we need to check element.getAttribute for SVG nodes
    var hidden = true;
    forEach(angular.element(element), function(element) {
      if ((' '  + (element.getAttribute('class') || '') + ' ').indexOf(' ng-hide ') === -1) {
        hidden = false;
      }
    });
    return hidden;
  }

  function MinErrMatcher(isNot, namespace, code, content, wording) {
    var codeRegex = new RegExp('^' + escapeRegexp('[' + namespace + ':' + code + ']'));
    var contentRegex = angular.isUndefined(content) || jasmine.isA_('RegExp', content) ?
        content : new RegExp(escapeRegexp(content));

    this.test = test;

    function escapeRegexp(str) {
      // This function escapes all special regex characters.
      // We use it to create matching regex from arbitrary strings.
      // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
      return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
    }

    function test(exception) {
      var exceptionMessage = (exception && exception.message) || exception || '';

      var codeMatches = codeRegex.test(exceptionMessage);
      var contentMatches = angular.isUndefined(contentRegex) || contentRegex.test(exceptionMessage);
      var matches = codeMatches && contentMatches;

      return {
        pass: isNot ? !matches : matches,
        message: message
      };

      function message() {
        return 'Expected ' + wording.inputType + (isNot ? ' not' : '') + ' to ' +
            wording.expectedAction + ' ' + namespace + 'MinErr(\'' + code + '\')' +
            (contentRegex ? ' matching ' + contentRegex.toString() : '') +
            (!exception ? '.' : ', but it ' + wording.actualAction + ': ' + exceptionMessage);
      }
    }
  }

  jasmine.addMatchers({
    toBeEmpty: cssMatcher('ng-empty', 'ng-not-empty'),
    toBeNotEmpty: cssMatcher('ng-not-empty', 'ng-empty'),
    toBeInvalid: cssMatcher('ng-invalid', 'ng-valid'),
    toBeValid: cssMatcher('ng-valid', 'ng-invalid'),
    toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'),
    toBePristine: cssMatcher('ng-pristine', 'ng-dirty'),
    toBeUntouched: cssMatcher('ng-untouched', 'ng-touched'),
    toBeTouched: cssMatcher('ng-touched', 'ng-untouched'),

    toBeAPromise: function() {
      return {
        compare: generateCompare(false),
        negativeCompare: generateCompare(true)
      };
      function generateCompare(isNot) {
        return function(actual) {
          var message = valueFn(
            'Expected object ' + (isNot ? 'not ' : '') + 'to be a promise');
          return { pass: isPromiseLike(actual), message: message };
        };
      }
    },

    toBeShown: function() {
      return {
        compare: generateCompare(false),
        negativeCompare: generateCompare(true)
      };
      function generateCompare(isNot) {
        return function(actual) {
          var message = valueFn('Expected element ' + (isNot ? '' : 'not ') + 'to have \'ng-hide\' class');
          var pass = !isNgElementHidden(actual);
          if (isNot) {
            pass = !pass;
          }
          return { pass: pass, message: message };
        };
      }
    },

    toBeHidden: function() {
      return {
        compare: generateCompare(false),
        negativeCompare: generateCompare(true)
      };
      function generateCompare(isNot) {
        return function(actual) {
          var message = valueFn('Expected element ' + (isNot ? 'not ' : '') + 'to have \'ng-hide\' class');
          var pass = isNgElementHidden(actual);
          if (isNot) {
            pass = !pass;
          }
          return { pass: pass, message: message };
        };
      }
    },

    toEqual: function(util) {
      return {
        compare: function(actual, expected) {
          if (actual && actual.$$log) {
            actual = (typeof expected === 'string')
                ? actual.toString()
                : actual.toArray();
          }
          return {
            pass: util.equals(actual, expected, [DOMTester])
          };
        }
      };
    },

    toEqualOneOf: function(util) {
      return {
        compare: function(actual) {
          var expectedArgs = Array.prototype.slice.call(arguments, 1);
          return {
            pass: expectedArgs.some(function(expected) {
              return util.equals(actual, expected, [DOMTester]);
            })
          };
        }
      };
    },

    toEqualData: function() {
      return {
        compare: function(actual, expected) {
          return { pass: angular.equals(actual, expected) };
        }
      };
    },

    toHaveBeenCalledOnce: function() {
      return {
        compare: function(actual) {
          if (arguments.length > 1) {
            throw new Error('`toHaveBeenCalledOnce` does not take arguments, ' +
                            'use `toHaveBeenCalledOnceWith`');
          }

          if (!jasmine.isSpy(actual)) {
            throw new Error('Expected a spy, but got ' + jasmine.pp(actual) + '.');
          }

          var count = actual.calls.count();
          var pass = count === 1;

          var message = function() {
            var msg = 'Expected spy ' + actual.and.identity() + (pass ? ' not ' : ' ') +
                      'to have been called once, but ';

            switch (count) {
              case 0:
                msg += 'it was never called.';
                break;
              case 1:
                msg += 'it was called once.';
                break;
              default:
                msg += 'it was called ' + count + ' times.';
                break;
            }

            return msg;
          };

          return {
            pass: pass,
            message: message
          };
        }
      };
    },

    toHaveBeenCalledOnceWith: function(util, customEqualityTesters) {
      return {
        compare: generateCompare(false),
        negativeCompare: generateCompare(true)
      };

      function generateCompare(isNot) {
        return function(actual) {
          if (!jasmine.isSpy(actual)) {
            throw new Error('Expected a spy, but got ' + jasmine.pp(actual) + '.');
          }

          var expectedArgs = Array.prototype.slice.call(arguments, 1);
          var actualCount = actual.calls.count();
          var actualArgs = actualCount && actual.calls.argsFor(0);

          var pass = (actualCount === 1) && util.equals(actualArgs, expectedArgs);
          if (isNot) pass = !pass;

          var message = function() {
            var msg = 'Expected spy ' + actual.and.identity() + (isNot ? ' not ' : ' ') +
                      'to have been called once with ' + jasmine.pp(expectedArgs) + ', but ';

            if (isNot) {
              msg += 'it was.';
            } else {
              switch (actualCount) {
                case 0:
                  msg += 'it was never called.';
                  break;
                case 1:
                  msg += 'it was called with ' + jasmine.pp(actualArgs) + '.';
                  break;
                default:
                  msg += 'it was called ' + actualCount + ' times.';
                  break;
              }
            }

            return msg;
          };

          return {
            pass: pass,
            message: message
          };
        };
      }
    },

    toBeOneOf: function() {
      return {
        compare: function(actual) {
          var expectedArgs = Array.prototype.slice.call(arguments, 1);
          return { pass: expectedArgs.indexOf(actual) !== -1 };
        }
      };
    },

    toHaveClass: function() {
      return {
        compare: generateCompare(false),
        negativeCompare: generateCompare(true)
      };
      function hasClass(element, selector) {
        if (!element.getAttribute) return false;
        return ((' ' + (element.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' ').
            indexOf(' ' + selector + ' ') > -1);
      }
      function generateCompare(isNot) {
        return function(actual, clazz) {
          var message = function() {
            return 'Expected \'' + angular.mock.dump(actual) + '\'' + (isNot ? ' not ' : '') + ' to have class \'' + clazz + '\'.';
          };
          var classes = clazz.trim().split(/\s+/);
          for (var i = 0; i < classes.length; ++i) {
            if (!hasClass(actual[0], classes[i])) {
              return { pass: isNot };
            }
          }
          return { pass: !isNot };
        };
      }
    },

    toEqualMinErr: function() {
      return {
        compare: generateCompare(false),
        negativeCompare: generateCompare(true)
      };

      function generateCompare(isNot) {
        return function(actual, namespace, code, content) {

          var matcher = new MinErrMatcher(isNot, namespace, code, content, {
            inputType: 'error',
            expectedAction: 'equal',
            actualAction: 'was'
          });

          return matcher.test(actual);
        };
      }
    },

    toThrowMinErr: function() {
      return {
        compare: generateCompare(false),
        negativeCompare: generateCompare(true)
      };

      function generateCompare(isNot) {
        return function(actual, namespace, code, content) {
          var exception;

          if (!angular.isFunction(actual)) {
            throw new Error('Actual is not a function');
          }

          try {
            actual();
          } catch (e) {
            exception = e;
          }

          var matcher = new MinErrMatcher(isNot, namespace, code, content, {
            inputType: 'function',
            expectedAction: 'throw',
            actualAction: 'threw'
          });

          return matcher.test(exception);
        };
      }
    },

    toBeMarkedAsSelected: function() {
      // Selected is special because the element property and attribute reflect each other's state.

      // Support: IE 9 only
      // IE9 will wrongly report hasAttribute('selected') === true when the property is
      // undefined or null, and the dev tools show that no attribute is set

      return {
        compare: function(actual) {
          var errors = [];
          var optionVal = toJson(actual.value);

          if (actual.selected === null || typeof actual.selected === 'undefined' || actual.selected === false) {
            errors.push('Expected option with value ' + optionVal + ' to have property "selected" set to truthy');
          }

          // Support: IE 9 only
          if (msie !== 9 && actual.hasAttribute('selected') === false) {
            errors.push('Expected option with value ' + optionVal + ' to have attribute "selected"');
          }

          var result = {
            pass: errors.length === 0,
            message: errors.join('\n')
          };

          return result;
        },
        negativeCompare: function(actual) {
          var errors = [];
          var optionVal = toJson(actual.value);

          if (actual.selected) {
            errors.push('Expected option with value ' + optionVal + ' property "selected" to be falsy');
          }

          // Support: IE 9 only
          if (msie !== 9 && actual.hasAttribute('selected')) {
            errors.push('Expected option with value ' + optionVal + ' not to have attribute "selected"');
          }

          var result = {
            pass: errors.length === 0,
            message: errors.join('\n')
          };

          return result;
        }
      };
    },
    toEqualSelect: function() {
      return {
        compare: function(actual, expected) {
          var actualValues = [],
              expectedValues = [].slice.call(arguments, 1);

          forEach(actual.find('option'), function(option) {
            actualValues.push(option.selected ? [option.value] : option.value);
          });

          var message = function() {
            return 'Expected ' + toJson(actualValues) + ' to equal ' + toJson(expectedValues) + '.';
          };

          return {
            pass: equals(expectedValues, actualValues),
            message: message
          };
        }
      };
    }
  });
});

/**
 * Create jasmine.Spy on given method, but ignore calls without arguments
 * This is helpful when need to spy only setter methods and ignore getters
 */
function spyOnlyCallsWithArgs(obj, method) {
  var originalFn = obj[method];
  var spy = spyOn(obj, method);
  obj[method] = function() {
    if (arguments.length) return spy.apply(this, arguments);
    return originalFn.apply(this);
  };
  return spy;
}

// Minimal implementation to mock what was removed from Jasmine 1.x
function createAsync(doneFn) {
  function Job() {
    this.next = [];
  }
  Job.prototype.done = function() {
    return this.runs(doneFn);
  };
  Job.prototype.runs = function(fn) {
    var newJob = new Job();
    this.next.push(function() {
      fn();
      newJob.start();
    });
    return newJob;
  };
  Job.prototype.waitsFor = function(fn, error, timeout) {
    var newJob = new Job();
    timeout = timeout || 5000;
    this.next.push(function() {
      var counter = 0,
        intervalId = window.setInterval(function() {
          if (fn()) {
            window.clearInterval(intervalId);
            newJob.start();
          }
          counter += 5;
          if (counter > timeout) {
            window.clearInterval(intervalId);
            throw new Error(error);
          }
        }, 5);
    });
    return newJob;
  };
  Job.prototype.waits = function(timeout) {
    return this.waitsFor(function() { return true; }, undefined, timeout);
  };
  Job.prototype.start = function() {
    var i;
    for (i = 0; i < this.next.length; i += 1) {
      this.next[i]();
    }
  };
  return new Job();
}

