var fs = Npm.require('fs');
var child_process = Npm.require('child_process');
var querystring = Npm.require('querystring');
var urlParser = Npm.require('url');

Spiderable = {};

// list of bot user agents that we want to serve statically, but do
// not obey the _escaped_fragment_ protocol. The page is served
// statically to any client whos user agent matches any of these
// regexps. Users may modify this array.
//
// An original goal with the spiderable package was to avoid doing
// user-agent based tests. But the reality is not enough bots support
// the _escaped_fragment_ protocol, so we need to hardcode a list
// here. I shed a silent tear.
Spiderable.userAgentRegExps = [
    /^facebookexternalhit/i, /^linkedinbot/i, /^twitterbot/i];

// how long to let phantomjs run before we kill it
var REQUEST_TIMEOUT = 15*1000;
// maximum size of result HTML. node's default is 200k which is too
// small for our docs.
var MAX_BUFFER = 5*1024*1024; // 5MB

WebApp.connectHandlers.use(function (req, res, next) {
  if (/\?.*_escaped_fragment_=/.test(req.url) ||
      _.any(Spiderable.userAgentRegExps, function (re) {
        return re.test(req.headers['user-agent']); })) {

    // reassembling url without escaped fragment if exists
    var parsedUrl = urlParser.parse(req.url);
    var parsedQuery = querystring.parse(parsedUrl.query);
    delete parsedQuery['_escaped_fragment_'];
    var newQuery = querystring.stringify(parsedQuery);
    var newPath = parsedUrl.pathname + (newQuery ? ('?' + newQuery) : '');
    var url = "http://" + req.headers.host + newPath;

    // This string is going to be put into a bash script, so it's important
    // that 'url' (which comes from the network) can neither exploit phantomjs
    // or the bash script. JSON stringification should prevent it from
    // exploiting phantomjs, and since the output of JSON.stringify shouldn't
    // be able to contain newlines, it should be unable to exploit bash as
    // well.
    var phantomScript = "var url = " + JSON.stringify(url) + ";" +
          "var page = require('webpage').create();" +
          "page.open(url);" +
          "setInterval(function() {" +
          "  var ready = page.evaluate(function () {" +
          "    if (typeof Meteor !== 'undefined' " +
          "        && typeof(Meteor.status) !== 'undefined' " +
          "        && Meteor.status().connected) {" +
          "      Deps.flush();" +
          "      return DDP._allSubscriptionsReady();" +
          "    }" +
          "    return false;" +
          "  });" +
          "  if (ready) {" +
          "    var out = page.content;" +
          "    out = out.replace(/<script[^>]+>(.|\\n|\\r)*?<\\/script\\s*>/ig, '');" +
          "    out = out.replace('<meta name=\"fragment\" content=\"!\">', '');" +
          "    console.log(out);" +
          "    phantom.exit();" +
          "  }" +
          "}, 100);\n";

    // Run phantomjs.
    //
    // Use '/dev/stdin' to avoid writing to a temporary file. We can't
    // just omit the file, as PhantomJS takes that to mean 'use a
    // REPL' and exits as soon as stdin closes.
    //
    // However, Node 0.8 broke the ability to open /dev/stdin in the
    // subprocess, so we can't just write our string to the process's stdin
    // directly; see https://gist.github.com/3751746 for the gory details. We
    // work around this with a bash heredoc. (We previous used a "cat |"
    // instead, but that meant we couldn't use exec and had to manage several
    // processes.)
    child_process.execFile(
      '/bin/bash',
      ['-c',
       ("exec phantomjs --load-images=no /dev/stdin <<'END'\n" +
        phantomScript + "END\n")],
      {timeout: REQUEST_TIMEOUT, maxBuffer: MAX_BUFFER},
      function (error, stdout, stderr) {
        if (!error && /<html/i.test(stdout)) {
          res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
          res.end(stdout);
        } else {
          // phantomjs failed. Don't send the error, instead send the
          // normal page.
          if (error && error.code === 127)
            Meteor._debug("spiderable: phantomjs not installed. Download and install from http://phantomjs.org/");
          else
            Meteor._debug("spiderable: phantomjs failed:", error, "\nstderr:", stderr);

          next();
        }
      });
  } else {
    next();
  }
});


var exists = convertElementSelectorToBranchedSelector(function (value) {

return operand ? exists : invertBranchedSelector(exists);

// Like _.isArray, but doesn't regard polyfilled Uint8Arrays on old browsers as
// arrays.
// XXX maybe this should be EJSON.isArray
isArray = function (x) {
  return _.isArray(x) && !EJSON.isBinary(x);
};

// XXX maybe this should be EJSON.isObject, though EJSON doesn't know about
// RegExp
// XXX note that _type(undefined) === 3!!!!
var isPlainObject = function (x) {
  return x && LocalCollection._f._type(x) === 3;
};

var isIndexable = function (x) {
  return isArray(x) || isPlainObject(x);
};

// If x is an array, true if f(e) is true for some e in x
//    (but never try f(x) directly)
// Otherwise, true if f(x) is true.
//
// Use this in cases where f(Array) should never be true...
// for example, equality comparisons to non-arrays,
// ordering comparisons (which should always be false if either side
// is an array), regexps (need string), mod (needs number)...
// XXX ensure comparisons are always false if LHS is an array
// XXX ensure comparisons among different types are false
var _anyIfArray = function (x, f) {
  if (isArray(x))
    return _.any(x, f);
  return f(x);
};

// True if f(x) is true, or x is an array and f(e) is true for some e in x.
//
// Use this for most operators where an array could satisfy the predicate.
var _anyIfArrayPlus = function (x, f) {
  if (f(x))
    return true;
  return isArray(x) && _.any(x, f);
};

var isOperatorObject = function (valueSelector) {
  if (!isPlainObject(valueSelector))
    return false;

  var theseAreOperators = undefined;
  _.each(valueSelector, function (value, selKey) {
    var thisIsOperator = selKey.substr(0, 1) === '$';
    if (theseAreOperators === undefined) {
      theseAreOperators = thisIsOperator;
    } else if (theseAreOperators !== thisIsOperator) {
      throw new Error("Inconsistent selector: " + valueSelector);
    }
  });
  return !!theseAreOperators;  // {} has no operators
};

var compileValueSelector = function (valueSelector, cursor) {
  if (valueSelector instanceof RegExp)
    return convertElementSelectorToBranchedSelector(
      regexpElementSelector(valueSelector));
  else if (isOperatorObject(valueSelector))
    return operatorValueSelector(valueSelector, cursor);
  else {
    return convertElementSelectorToBranchedSelector(
      equalityElementSelector(valueSelector));
  }
};

var regexpElementSelector = function (regexp) {
  return function (value) {
    if (value instanceof RegExp) {
      // Comparing two regexps means seeing if the regexps are identical
      // (really!). Underscore knows how.
      return _.isEqual(value, regexp);
    }
    // Regexps only work against strings.
    if (typeof value !== 'string')
      return false;
    return regexp.test(value);
  };
};

var convertElementSelectorToBranchedSelector = function (
    elementSelector, options) {
  options = options || {};
  return GROK(function (branches, wholeDoc) {
    var expanded = branches;
    if (!options.dontExpandLeafArrays)
      expanded = expandArraysInBranches(branches);
    var result = _.any(expanded, function (element) {
      // XXX arrayIndex!  need to save the winner here
      return elementSelector(element.value, wholeDoc);
    });
    return {result: result};
  });
};

var equalityElementSelector = function (elementSelector) {
  if (isOperatorObject(elementSelector))
    throw Error("Can't create equalityValueSelector for operator object");

  // Special-case: null and undefined are equal (if you got undefined in there
  // somewhere, or if you got it due to some branch being non-existent in the
  // weird special case), even though they aren't with EJSON.equals.
  if (elementSelector == null) {  // undefined or null
    return function (value) {
      return value == null;  // undefined or null
    };
  }

  return function (value) {
    return LocalCollection._f._equal(elementSelector, value);
  };
};

// XXX this won't be necessary
var GROK = function (f) {
  f._groksExtendedLookup_ = true;
  return f;
};

// XXX get rid of cursor when possible
var operatorValueSelector = function (valueSelector, cursor) {
  // XXX kill this soon
  if (!_.all(valueSelector, function (operand, operator) {
    return _.has(ELEMENT_OPERATORS, operator) ||
      _.has(VALUE_OPERATORS, operator);
  })) {
    return operatorValueSelectorLegacy(valueSelector, cursor);
  }

  // Each valueSelector works separately on the various branches.  So one
  // operator can match one branch and another can match another branch.  This
  // is OK.

  var operatorFunctions = [];
  _.each(valueSelector, function (operand, operator) {
    if (_.has(VALUE_OPERATORS, operator)) {
      operatorFunctions.push(
        VALUE_OPERATORS[operator](operand, valueSelector, cursor));
    } else if (_.has(ELEMENT_OPERATORS, operator)) {
      // XXX justify three arguments
      var options = ELEMENT_OPERATORS[operator];
      if (typeof options === 'function')
        options = {elementSelector: options};
      operatorFunctions.push(
        convertElementSelectorToBranchedSelector(
          options.elementSelector(operand, valueSelector, cursor),
          options));
    } else {
      throw new Error("Unrecognized operator: " + operator);
    }
  });
  // NB: this is very similar to andCompiledDocumentSelectors but that one
  // "assumes" the first arg is a doc and here it "assumes" it's a branched
  // value list. The code is identical for now, though.
  return GROK(function (branches, doc) {
    // XXX arrayIndex!
    var result = _.all(operatorFunctions, function (f) {
      return f(branches, doc).result;
    });
    return {result: result};
  });
};

// XXX drop "cursor" when _distance is improved
var compileArrayOfDocumentSelectors = function (selectors, cursor) {
  if (!isArray(selectors) || _.isEmpty(selectors))
    throw Error("$and/$or/$nor must be nonempty array");
  return _.map(selectors, function (subSelector) {
    if (!isPlainObject(subSelector))
      throw Error("$or/$and/$nor entries need to be full objects");
    return compileDocumentSelector(subSelector, cursor);
  });
};

// XXX can factor out common logic below
var LOGICAL_OPERATORS = {
  $and: function(subSelector, cursor) {
    var selectors = compileArrayOfDocumentSelectors(subSelector, cursor);
    return andCompiledDocumentSelectors(selectors);
  },

  $or: function(subSelector, cursor) {
    var selectors = compileArrayOfDocumentSelectors(subSelector, cursor);
    // XXX remove wholeDoc later
    return function (doc, wholeDoc) {
      var result = _.any(selectors, function (f) {
        return f(doc, wholeDoc).result;
      });
      // XXX arrayIndex!
      return {result: result};
    };
  },

  $nor: function(subSelector, cursor) {
    var selectors = compileArrayOfDocumentSelectors(subSelector, cursor);
    // XXX remove wholeDoc later
    return function (doc, wholeDoc) {
      var result = _.all(selectors, function (f) {
        return !f(doc, wholeDoc).result;
      });
      // Never set arrayIndex, because we only match if nothing in particular
      // "matched".
      return {result: result};
    };
  },

  $where: function(selectorValue) {
    if (!(selectorValue instanceof Function)) {
      // XXX MongoDB seems to have more complex logic to decide where or or not
      // to add "return"; not sure exactly what it is.
      selectorValue = Function("obj", "return " + selectorValue);
    }
    return function (doc) {
      // We make the document available as both `this` and `obj`.
      // XXX not sure what we should do if this throws
      return {result: selectorValue.call(doc, doc)};
    };
  },

  // This is just used as a comment in the query (in MongoDB, it also ends up in
  // query logs); it has no effect on the actual selection.
  $comment: function () {
    return function () {
      return {result: true};
    };
  }
};

var invertBranchedSelector = function (branchedSelector) {
  // Note that this implicitly "deMorganizes" the wrapped function.  ie, it
  // means that ALL branch values need to fail to match innerBranchedSelector.
  return function (branchValues, doc) {
    var invertMe = branchedSelector(branchValues, doc);
    // We explicitly choose to strip arrayIndex here: it doesn't make sense to
    // say "update the array element that does not match something", at least
    // in mongo-land.
    return {result: !invertMe.result};
  };
};

// XXX doc
var VALUE_OPERATORS = {
  $not: function (operand, operator, cursor) {
    return invertBranchedSelector(compileValueSelector(operand, cursor));
  },
  $ne: function (operand) {
    return invertBranchedSelector(convertElementSelectorToBranchedSelector(
      equalityElementSelector(operand)));
  },
  $nin: function (operand) {
    return invertBranchedSelector(convertElementSelectorToBranchedSelector(
      ELEMENT_OPERATORS.$in(operand)));
  }
};

var makeInequality = function (cmpValueComparator) {
  return function (operand) {
    // Arrays never compare false with non-arrays for any inequality.
    if (isArray(operand)) {
      return function () {
        return false;
      };
    }

    // Special case: consider undefined and null the same (so true with
    // $gte/$lte).
    if (operand === undefined)
      operand = null;

    var operandType = LocalCollection._f._type(operand);

    return function (value) {
      if (value === undefined)
        value = null;
      // Comparisons are never true among things of different type (except null
      // vs undefined).
      if (LocalCollection._f._type(value) !== operandType)
        return false;
      return cmpValueComparator(LocalCollection._f._cmp(value, operand));
    };
  };
};

// XXX redoc
// Each value operator is a function with args:
//  - operand - Anything
//  - operators - Object - operators on the same level (neighbours)
//  - cursor - Object - original cursor
// returns a function with args:
//  - value - a value the operator is tested against
//  - doc - the whole document tested in this query
var ELEMENT_OPERATORS = {
  $lt: makeInequality(function (cmpValue) {
    return cmpValue < 0;
  }),
  $gt: makeInequality(function (cmpValue) {
    return cmpValue > 0;
  }),
  $lte: makeInequality(function (cmpValue) {
    return cmpValue <= 0;
  }),
  $gte: makeInequality(function (cmpValue) {
    return cmpValue >= 0;
  }),
  $mod: function (operand) {
    if (!(isArray(operand) && operand.length === 2
          && typeof(operand[0]) === 'number'
          && typeof(operand[1]) === 'number')) {
      throw Error("argument to $mod must be an array of two numbers");
    }
    // XXX could require to be ints or round or something
    var divisor = operand[0];
    var remainder = operand[1];
    return function (value) {
      return typeof value === 'number' && value % divisor === remainder;
    };
  },
  $in: function (operand) {
    if (!isArray(operand))
      throw Error("$in needs an array");

    var elementSelectors = [];
    _.each(operand, function (option) {
      if (option instanceof RegExp)
        elementSelectors.push(regexpElementSelector(option));
      else if (isOperatorObject(option))
        throw Error("cannot nest $ under $in");
      else
        elementSelectors.push(equalityElementSelector(option));
    });

    return function (value) {
      // Allow {a: {$in: [null]}} to match when 'a' does not exist.
      if (value === undefined)
        value = null;
      return _.any(elementSelectors, function (e) {
        return e(value);
      });
    };
  },
  $size: {
    // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we
    // don't want to consider the element [5,5] in the leaf array [[5,5]] as a
    // possible value.
    dontExpandLeafArrays: true,
    elementSelector: function (operand) {
      if (typeof operand === 'string') {
        // Don't ask me why, but by experimentation, this seems to be what Mongo
        // does.
        operand = 0;
      } else if (typeof operand !== 'number') {
        throw Error("$size needs a number");
      }
      return function (value) {
        return isArray(value) && value.length === operand;
      };
    }
  }
};

var LEGACY_VALUE_OPERATORS = {
  "$in": function (operand) {
    if (!isArray(operand))
      throw new Error("Argument to $in must be array");
    return function (value) {
      return _anyIfArrayPlus(value, function (x) {
        return _.any(operand, function (operandElt) {
          return LocalCollection._f._equal(operandElt, x);
        });
      });
    };
  },

  "$all": function (operand) {
    if (!isArray(operand))
      throw new Error("Argument to $all must be array");
    return function (value) {
      if (!isArray(value))
        return false;
      return _.all(operand, function (operandElt) {
        return _.any(value, function (valueElt) {
          return LocalCollection._f._equal(operandElt, valueElt);
        });
      });
    };
  },

  "$lt": function (operand) {
    return function (value) {
      return _anyIfArray(value, function (x) {
        return LocalCollection._f._cmp(x, operand) < 0;
      });
    };
  },

  "$lte": function (operand) {
    return function (value) {
      return _anyIfArray(value, function (x) {
        return LocalCollection._f._cmp(x, operand) <= 0;
      });
    };
  },

  "$gt": function (operand) {
    return function (value) {
      return _anyIfArray(value, function (x) {
        return LocalCollection._f._cmp(x, operand) > 0;
      });
    };
  },

  "$gte": function (operand) {
    return function (value) {
      return _anyIfArray(value, function (x) {
        return LocalCollection._f._cmp(x, operand) >= 0;
      });
    };
  },

  "$ne": function (operand) {
    return function (value) {
      return ! _anyIfArrayPlus(value, function (x) {
        return LocalCollection._f._equal(x, operand);
      });
    };
  },

  "$nin": function (operand) {
    if (!isArray(operand))
      throw new Error("Argument to $nin must be array");
    var inFunction = LEGACY_VALUE_OPERATORS.$in(operand);
    return function (value, doc) {
      // Field doesn't exist, so it's not-in operand
      if (value === undefined)
        return true;
      return !inFunction(value, doc);
    };
  },

  "$exists": function (operand) {
    return function (value) {
      return operand === (value !== undefined);
    };
  },

  "$mod": function (operand) {
    var divisor = operand[0],
        remainder = operand[1];
    return function (value) {
      return _anyIfArray(value, function (x) {
        return x % divisor === remainder;
      });
    };
  },

  "$size": function (operand) {
    return function (value) {
      return isArray(value) && operand === value.length;
    };
  },

  "$type": function (operand) {
    return function (value) {
      // A nonexistent field is of no type.
      if (value === undefined)
        return false;
      // Definitely not _anyIfArrayPlus: $type: 4 only matches arrays that have
      // arrays as elements according to the Mongo docs.
      return _anyIfArray(value, function (x) {
        return LocalCollection._f._type(x) === operand;
      });
    };
  },

  "$regex": function (operand, operators) {
    var options = operators.$options;
    if (options !== undefined) {
      // Options passed in $options (even the empty string) always overrides
      // options in the RegExp object itself. (See also
      // Meteor.Collection._rewriteSelector.)

      // Be clear that we only support the JS-supported options, not extended
      // ones (eg, Mongo supports x and s). Ideally we would implement x and s
      // by transforming the regexp, but not today...
      if (/[^gim]/.test(options))
        throw new Error("Only the i, m, and g regexp options are supported");

      var regexSource = operand instanceof RegExp ? operand.source : operand;
      operand = new RegExp(regexSource, options);
    } else if (!(operand instanceof RegExp)) {
      operand = new RegExp(operand);
    }

    return function (value) {
      if (value === undefined)
        return false;
      return _anyIfArray(value, function (x) {
        return operand.test(x);
      });
    };
  },

  "$options": function (operand) {
    // evaluation happens at the $regex function above
    return function (value) { return true; };
  },

  "$elemMatch": function (operand, selector, cursor) {
    var matcher = compileDocumentSelector(operand, cursor);
    return function (value, doc) {
      if (!isArray(value))
        return false;
      return _.any(value, function (x) {
        return matcher(x, doc).result;
      });
    };
  },

  "$not": function (operand, operators, cursor) {
    var matcher = compileValueSelector(operand, cursor);
    return function (value, doc) {
      return !matcher(value, doc);
    };
  },

  "$near": function (operand, operators, cursor) {
    function distanceCoordinatePairs (a, b) {
      a = pointToArray(a);
      b = pointToArray(b);
      var x = a[0] - b[0];
      var y = a[1] - b[1];
      if (_.isNaN(x) || _.isNaN(y))
        return null;
      return Math.sqrt(x * x + y * y);
    }
    // Makes sure we get 2 elements array and assume the first one to be x and
    // the second one to y no matter what user passes.
    // In case user passes { lon: x, lat: y } returns [x, y]
    function pointToArray (point) {
      return _.map(point, _.identity);
    }
    // GeoJSON query is marked as $geometry property
    var mode = _.isObject(operand) && _.has(operand, '$geometry') ? "2dsphere" : "2d";
    var maxDistance = mode === "2d" ? operators.$maxDistance : operand.$maxDistance;
    var point = mode === "2d" ? operand : operand.$geometry;
    return function (value, doc) {
      var dist = null;
      switch (mode) {
        case "2d":
          dist = distanceCoordinatePairs(point, value);
          break;
        case "2dsphere":
          // XXX: for now, we don't calculate the actual distance between, say,
          // polygon and circle. If people care about this use-case it will get
          // a priority.
          if (value.type === "Point")
            dist = GeoJSON.pointDistance(point, value);
          else
            dist = GeoJSON.geometryWithinRadius(value, point, maxDistance) ?
                     0 : maxDistance + 1;
          break;
      }
      // Used later in sorting by distance, since $near queries are sorted by
      // distance from closest to farthest.
      if (cursor) {
        if (!cursor._distance)
          cursor._distance = {};
        cursor._distance[doc._id] = dist;
      }

      // Distance couldn't parse a geometry object
      if (dist === null)
        return false;

      return maxDistance === undefined ? true : dist <= maxDistance;
    };
  },

  "$maxDistance": function () {
    // evaluation happens in the $near operator
    return function () { return true; }
  }
};

var operatorValueSelectorLegacy = function (valueSelector, cursor) {
  var operatorFunctions = [];
  _.each(valueSelector, function (operand, operator) {
    if (!_.has(LEGACY_VALUE_OPERATORS, operator))
      throw new Error("Unrecognized legacy operator: " + operator);
    // Special case for location operators
    operatorFunctions.push(LEGACY_VALUE_OPERATORS[operator](
      operand, valueSelector, cursor));
  });
  return function (value, doc) {
    return _.all(operatorFunctions, function (f) {
      return f(value, doc);
    });
  };
};

// helpers used by compiled selector code
LocalCollection._f = {
  // XXX for _all and _in, consider building 'inquery' at compile time..

  _type: function (v) {
    if (typeof v === "number")
      return 1;
    if (typeof v === "string")
      return 2;
    if (typeof v === "boolean")
      return 8;
    if (isArray(v))
      return 4;
    if (v === null)
      return 10;
    if (v instanceof RegExp)
      // note that typeof(/x/) === "object"
      return 11;
    if (typeof v === "function")
      return 13;
    if (v instanceof Date)
      return 9;
    if (EJSON.isBinary(v))
      return 5;
    if (v instanceof LocalCollection._ObjectID)
      return 7;
    return 3; // object

    // XXX support some/all of these:
    // 14, symbol
    // 15, javascript code with scope
    // 16, 18: 32-bit/64-bit integer
    // 17, timestamp
    // 255, minkey
    // 127, maxkey
  },

  // deep equality test: use for literal document and array matches
  _equal: function (a, b) {
    return EJSON.equals(a, b, {keyOrderSensitive: true});
  },

  // maps a type code to a value that can be used to sort values of
  // different types
  _typeorder: function (t) {
    // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types
    // XXX what is the correct sort position for Javascript code?
    // ('100' in the matrix below)
    // XXX minkey/maxkey
    return [-1,  // (not a type)
            1,   // number
            2,   // string
            3,   // object
            4,   // array
            5,   // binary
            -1,  // deprecated
            6,   // ObjectID
            7,   // bool
            8,   // Date
            0,   // null
            9,   // RegExp
            -1,  // deprecated
            100, // JS code
            2,   // deprecated (symbol)
            100, // JS code
            1,   // 32-bit int
            8,   // Mongo timestamp
            1    // 64-bit int
           ][t];
  },

  // compare two values of unknown type according to BSON ordering
  // semantics. (as an extension, consider 'undefined' to be less than
  // any other value.) return negative if a is less, positive if b is
  // less, or 0 if equal
  _cmp: function (a, b) {
    if (a === undefined)
      return b === undefined ? 0 : -1;
    if (b === undefined)
      return 1;
    var ta = LocalCollection._f._type(a);
    var tb = LocalCollection._f._type(b);
    var oa = LocalCollection._f._typeorder(ta);
    var ob = LocalCollection._f._typeorder(tb);
    if (oa !== ob)
      return oa < ob ? -1 : 1;
    if (ta !== tb)
      // XXX need to implement this if we implement Symbol or integers, or
      // Timestamp
      throw Error("Missing type coercion logic in _cmp");
    if (ta === 7) { // ObjectID
      // Convert to string.
      ta = tb = 2;
      a = a.toHexString();
      b = b.toHexString();
    }
    if (ta === 9) { // Date
      // Convert to millis.
      ta = tb = 1;
      a = a.getTime();
      b = b.getTime();
    }

    if (ta === 1) // double
      return a - b;
    if (tb === 2) // string
      return a < b ? -1 : (a === b ? 0 : 1);
    if (ta === 3) { // Object
      // this could be much more efficient in the expected case ...
      var to_array = function (obj) {
        var ret = [];
        for (var key in obj) {
          ret.push(key);
          ret.push(obj[key]);
        }
        return ret;
      };
      return LocalCollection._f._cmp(to_array(a), to_array(b));
    }
    if (ta === 4) { // Array
      for (var i = 0; ; i++) {
        if (i === a.length)
          return (i === b.length) ? 0 : -1;
        if (i === b.length)
          return 1;
        var s = LocalCollection._f._cmp(a[i], b[i]);
        if (s !== 0)
          return s;
      }
    }
    if (ta === 5) { // binary
      // Surprisingly, a small binary blob is always less than a large one in
      // Mongo.
      if (a.length !== b.length)
        return a.length - b.length;
      for (i = 0; i < a.length; i++) {
        if (a[i] < b[i])
          return -1;
        if (a[i] > b[i])
          return 1;
      }
      return 0;
    }
    if (ta === 8) { // boolean
      if (a) return b ? 0 : 1;
      return b ? -1 : 0;
    }
    if (ta === 10) // null
      return 0;
    if (ta === 11) // regexp
      throw Error("Sorting not supported on regular expression"); // XXX
    // 13: javascript code
    // 14: symbol
    // 15: javascript code with scope
    // 16: 32-bit integer
    // 17: timestamp
    // 18: 64-bit integer
    // 255: minkey
    // 127: maxkey
    if (ta === 13) // javascript code
      throw Error("Sorting not supported on Javascript code"); // XXX
    throw Error("Unknown type to sort");
  }
};

// For unit tests. True if the given document matches the given
// selector.
MinimongoTest.matches = function (selector, doc) {
  return (LocalCollection._compileSelector(selector))(doc).result;
};

// string can be converted to integer
numericKey = function (s) {
  return /^[0-9]+$/.test(s);
};

// XXX redoc
// XXX be aware that _compileSort currently assumes that lookup functions
//     return non-empty arrays but that is no longer the case
// _makeLookupFunction(key) returns a lookup function.
//
// A lookup function takes in a document and returns an array of matching
// values.  If no arrays are found while looking up the key, this array will
// have exactly one value (possibly 'undefined', if some segment of the key was
// not found).
//
// If arrays are found in the middle, this can have more than one element, since
// we "branch". When we "branch", if there are more key segments to look up,
// then we only pursue branches that are plain objects (not arrays or scalars).
// This means we can actually end up with no entries!
//
// At the top level, you may only pass in a plain object.
//
// _makeLookupFunction('a.x')({a: {x: 1}}) returns [1]
// _makeLookupFunction('a.x')({a: {x: [1]}}) returns [[1]]
// _makeLookupFunction('a.x')({a: 5})  returns [undefined]
// _makeLookupFunction('a.x')({a: [5]})  returns []
// _makeLookupFunction('a.x')({a: [{x: 1},
//                                 [],
//                                 4,
//                                 {x: [2]},
//                                 {y: 3}]})
//   returns [1, [2], undefined]
LocalCollection._makeLookupFunction2 = function (key) {
  var parts = key.split('.');
  var firstPart = parts.length ? parts[0] : '';
  var firstPartIsNumeric = numericKey(firstPart);
  var lookupRest;
  if (parts.length > 1) {
    lookupRest = LocalCollection._makeLookupFunction2(parts.slice(1).join('.'));
  }

  // Doc will always be a plain object or an array.
  // apply an explicit numeric index, an array.
  return function (doc, firstArrayIndex) {
    if (isArray(doc)) {
      // If we're being asked to do an invalid lookup into an array (non-integer
      // or out-of-bounds), return no results (which is different from returning
      // a single undefined result, in that `null` equality checks won't match).
      if (!(firstPartIsNumeric && firstPart < doc.length))
        return [];

      // If this is the first array index we've seen, remember the index.
      // (Mongo doesn't support multiple uses of '$', at least not in 2.5.
      if (firstArrayIndex === undefined)
        firstArrayIndex = +firstPart;
    }

    // Do our first lookup.
    var firstLevel = doc[firstPart];

    // If there is no deeper to dig, return what we found.
    //
    // If what we found is an array, most value selectors will choose to treat
    // the elements of the array as matchable values in their own right, but
    // that's done outside of the lookup function. (Exceptions to this are $size
    // and stuff relating to $elemMatch.  eg, {a: {$size: 2}} does not match {a:
    // [[1, 2]]}.)
    //
    // That said, if we just did an *explicit* array lookup (on doc) to find
    // firstLevel, and firstLevel is an array too, we do NOT want value
    // selectors to iterate over it.  eg, {'a.0': 5} does not match {a: [[5]]}.
    // So in that case, we mark the return value as "don't iterate".
    if (!lookupRest) {
      return [{value: firstLevel,
               dontIterate: isArray(doc) && isArray(firstLevel),
               arrayIndex: firstArrayIndex}];
    }

    // We need to dig deeper.  But if we can't, because what we've found is not
    // an array or plain object, we're done. If we just did a numeric index into
    // an array, we return nothing here (this is a change in Mongo 2.5 from
    // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise,
    // return a single `undefined` (which can, for example, match via equality
    // with `null`).
    if (!isIndexable(firstLevel)) {
      return isArray(doc) ? [] : [{value: undefined,
                                   arrayIndex: firstArrayIndex}];
    }

    var result = [];
    var appendToResult = function (more) {
      Array.prototype.push.apply(result, more);
    };

    // Dig deeper: look up the rest of the parts on whatever we've found.
    // (lookupRest is smart enough to not try to do invalid lookups into
    // firstLevel if it's an array.)
    appendToResult(lookupRest(firstLevel, firstArrayIndex));

    // If we found an array, then in *addition* to potentially treating the next
    // part as a literal integer lookup, we should also "branch": try to do look
    // up the rest of the parts on each array element in parallel.
    //
    // In this case, we *only* dig deeper into array elements that are plain
    // objects. (Recall that we only got this far if we have further to dig.)
    // This makes sense: we certainly don't dig deeper into non-indexable
    // objects. And it would be weird to dig into an array: it's simpler to have
    // a rule that explicit integer indexes only apply to an outer array, not to
    // an array you find after a branching search.
    if (isArray(firstLevel)) {
      _.each(firstLevel, function (branch, arrayIndex) {
        if (isPlainObject(branch)) {
          appendToResult(lookupRest(
            branch,
            firstArrayIndex === undefined ? arrayIndex : firstArrayIndex));
        }
      });
    }

    return result;
  };
};

LocalCollection._makeLookupFunction = function (key) {
  var real = LocalCollection._makeLookupFunction2(key);
  return function (doc) {
    return _.pluck(real(doc), 'value');
  };
};

var expandArraysInBranches = function (branches) {
  var branchesOut = [];
  _.each(branches, function (branch) {
    branchesOut.push({
      value: branch.value,
      arrayIndex: branch.arrayIndex
    });
    if (isArray(branch.value) && !branch.dontIterate) {
      _.each(branch.value, function (leaf, i) {
        branchesOut.push({
          value: leaf,
          arrayIndex: branch.arrayIndex === undefined ? i : branch.arrayIndex
        });
      });
    }
  });
  return branchesOut;
};

// The main compilation function for a given selector.
var compileDocumentSelector = function (docSelector, cursor) {
  var perKeySelectors = [];
  _.each(docSelector, function (subSelector, key) {
    if (key.substr(0, 1) === '$') {
      // Outer operators are either logical operators (they recurse back into
      // this function), or $where.
      if (!_.has(LOGICAL_OPERATORS, key))
        throw new Error("Unrecognized logical operator: " + key);
      // XXX rename perKeySelectors
      perKeySelectors.push(
        LOGICAL_OPERATORS[key](subSelector, cursor));
    } else {
      var lookUpByIndex = LocalCollection._makeLookupFunction2(key);
      var valueSelectorFunc =
        compileValueSelector(subSelector, cursor);
      perKeySelectors.push(function (doc, wholeDoc) {
        var branchValues = lookUpByIndex(doc);

        if (valueSelectorFunc._groksExtendedLookup_) {
          return valueSelectorFunc(branchValues, wholeDoc);
        }

        // XXX get rid of this all later
        branchValues = _.pluck(branchValues, 'value');
        // We apply the selector to each "branched" value and return true if any
        // match. However, for "negative" selectors like $ne or $not we actually
        // require *all* elements to match.
        //
        // This is because {'x.tag': {$ne: "foo"}} applied to {x: [{tag: 'foo'},
        // {tag: 'bar'}]} should NOT match even though there is a branch that
        // matches. (This matches the fact that $ne uses a negated
        // _anyIfArrayPlus, for when the last level of the key is the array,
        // which deMorgans into an 'all'.)
        //
        // XXX This isn't 100% consistent with MongoDB in 'null' cases:
        //     https://jira.mongodb.org/browse/SERVER-8585
        // XXX this still isn't right.  consider {a: {$ne: 5, $gt: 6}}. the
        //     $ne needs to use the "all" logic and the $gt needs the "any"
        //     logic.  (There is an expect_fail test for this now.)
        // XXX add test for this brokenness
        var combiner = (subSelector &&
                        (subSelector.$not || subSelector.$ne ||
                         subSelector.$nin))
              ? _.all : _.any;
        var result = combiner(branchValues, function (val) {
          return valueSelectorFunc(val, wholeDoc);
        });
        // XXX rewrite value selectors to use structured return
        return {result: result};
      });
    }
  });

  return andCompiledDocumentSelectors(perKeySelectors);
};

// Given a selector, return a function that takes one argument, a
// document. It returns an object with fields
//    - result: bool, true if the document matches the selector
// XXX add "arrayIndex" for use by update with '$'
LocalCollection._compileSelector = function (selector, cursor) {
  // you can pass a literal function instead of a selector
  if (selector instanceof Function)
    return function (doc) {
      return {result: !!selector.call(doc)};
    };

  // shorthand -- scalars match _id
  if (LocalCollection._selectorIsId(selector)) {
    return function (doc) {
      return {result: EJSON.equals(doc._id, selector)};
    };
  }

  // protect against dangerous selectors.  falsey and {_id: falsey} are both
  // likely programmer error, and not what you want, particularly for
  // destructive operations.
  if (!selector || (('_id' in selector) && !selector._id))
    return matchesNothingSelector;

  // Top level can't be an array or true or binary.
  if (typeof(selector) === 'boolean' || isArray(selector) ||
      EJSON.isBinary(selector))
    throw new Error("Invalid selector: " + selector);

  // XXX get rid of second argument once _distance refactored
  var s = compileDocumentSelector(selector, cursor);
  return function (doc) {
    return s(doc, doc);
  };
};

var matchesNothingSelector = function (doc) {
  return {result: false};
};

var andCompiledDocumentSelectors = function (selectors) {
  // XXX simplify to not involve 'arguments' once _distance is refactored
  return function (/*doc, sometimes wholeDoc*/) {
    var args = _.toArray(arguments);
    // XXX arrayIndex!
    var result = _.all(selectors, function (f) {
      return f.apply(null, args).result;
    });
    return {result: result};
  };
};

// Remaining to update:
// - $exists
// - $type
// - $regex/$option
// - $all
// - $elemMatch
// - $near/$maxDistance


var OplogCollection = new Meteor.Collection("oplog-" + Random.id());

Tinytest.add("mongo-livedata - oplog - cursorSupported", function (test) {
  var supported = function (expected, selector) {
    var cursor = OplogCollection.find(selector);
    test.equal(
      MongoTest.OplogObserveDriver.cursorSupported(cursor._cursorDescription),
      expected);
  };

  supported(true, "asdf");
  supported(true, 1234);
  supported(true, new Meteor.Collection.ObjectID());

  supported(true, {_id: "asdf"});
  supported(true, {_id: 1234});
  supported(true, {_id: new Meteor.Collection.ObjectID()});

  supported(true, {foo: "asdf",
                   bar: 1234,
                   baz: new Meteor.Collection.ObjectID(),
                   eeney: true,
                   miney: false,
                   moe: null});

  supported(true, {});

  supported(false, {$and: [{foo: "asdf"}, {bar: "baz"}]});
  supported(false, {foo: {x: 1}});
  supported(false, {foo: {$gt: 1}});
  supported(false, {foo: [1, 2, 3]});
});


var Future = require('fibers/future');
var files = require('./files.js');
var path = require('path');
var fs = require('fs');
var unipackage = require('./unipackage.js');
var fiberHelpers = require('./fiber-helpers.js');
var Fiber = require('fibers');
var httpHelpers = require('./http-helpers.js');
var _ = require('underscore');

// a bit of a hack
var getPackage = _.once(function (context) {
  return unipackage.load({
    library: context.library,
    packages: [ 'meteor', 'livedata' ],
    release: context.releaseVersion
  });
});

var getGalaxy = _.once(function (context) {
  var Package = getPackage(context);
  if (!context.galaxy) {
    process.stderr.write("Could not find a deploy endpoint. " +
                         "You can set the GALAXY environment variable, " +
                         "or configure your site's DNS to resolve to " +
                         "your Galaxy's proxy.\n");
    process.exit(1);
  }

  var galaxy = Package.livedata.DDP.connect(context.galaxy.url);
  var timeout = Package.meteor.Meteor.setTimeout(function () {
    if (galaxy.status().status !== "connected") {
      process.stderr.write("Could not connect to galaxy " + context.galaxy.url
                           + ": " + galaxy.status().status + '\n');
      process.exit(1);
    }
  }, 10*1000);
  var close = galaxy.close;
  galaxy.close = function (/*arguments*/) {
    Package.meteor.Meteor.clearTimeout(timeout);
    close.apply(galaxy, arguments);
  };
  return galaxy;
});

var exitWithError = function (error, messages) {
  messages = messages || {};
  var msg = messages[error.error];
  if (msg)
    process.stderr.write(msg + "\n");
  else if (error.message)
    process.stderr.write("Denied: " + error.message + "\n");

  process.exit(1);
};

// XXX copied from galaxy/tool/galaxy.js
var prettyCall = function (galaxy, name, args, messages) {
  try {
    var ret = galaxy.apply(name, args);
  } catch (e) {
    exitWithError(e, messages);
  }
  return ret;
};

var prettySub = function (galaxy, name, args, messages) {
  var onError = function (e) {
    exitWithError(e, messages);
  };

  try {
    var ret = galaxy._subscribeAndWait(name, args, {onLateError: onError});
  } catch (e) {
    onError(e);
  }
  return ret;
};

exports.discoverGalaxy = function (app) {
  app = app + ":" + (process.env.DISCOVERY_PORT || 443);
  var url = "https://" + app + "/_GALAXY_";
  var fut = new Future();

  if (process.env.GALAXY)
    return process.env.GALAXY;

  // At some point we may want to send a version in the request so that galaxy
  // can respond differently to different versions of meteor.
  httpHelpers.request({
    url: url,
    json: true,
    strictSSL: true,
    // We don't want to be confused by, eg, a non-Galaxy-hosted site which
    // redirects to a Galaxy-hosted site.
    followRedirect: false
  }, function (err, resp, body) {
    if (! err &&
        resp.statusCode === 200 &&
        body &&
        _.has(body, "galaxyDiscoveryVersion") &&
        _.has(body, "galaxyUrl") &&
        (body.galaxyDiscoveryVersion === "galaxy-discovery-pre0")) {
      fut.return(body.galaxyUrl);
    } else {
      fut.return(null);
    }
  });
  return fut.wait();
};

exports.deleteApp = function (app, context) {
  var galaxy = getGalaxy(context);
  galaxy.call("destroyApp", app);
  galaxy.close();
  process.stdout.write("Deleted.\n");
};

// options:
// - context
// - app
// - appDir
// - settings
// - bundleOptions
// - starball
// XXX refactor this to separate the "maybe bundle" part from "actually deploy"
//     so we can be careful to not rely on any of the app dir context when
//     in --star mode.
exports.deploy = function (options) {
  var tmpdir = files.mkdtemp('deploy');
  var buildDir = path.join(tmpdir, 'build');
  var topLevelDirName = path.basename(options.appDir);
  var bundlePath = path.join(buildDir, topLevelDirName);
  var bundler = require('./bundler.js');
  var starball;

  // Don't try to connect to galaxy before the bundle is done. Because bundling
  // doesn't yield, this will cause the connection to timeout. Eventually we'd
  // like to have bundle yield, so that we can connect (and make sure auth
  // works) before bundling.

  if (!options.starball) {
    process.stdout.write('Deploying ' + options.app + '. Bundling...\n');
    var bundleResult = bundler.bundle(options.appDir, bundlePath,
                                      options.bundleOptions);
    if (bundleResult.errors) {
      process.stdout.write("\n\nErrors prevented deploying:\n");
      process.stdout.write(bundleResult.errors.formatMessages());
      process.exit(1);
    }

    // S3 (which is what's likely on the other end our upload) requires
    // a content-length header for HTTP PUT uploads. That means that we
    // have to actually tgz up the bundle before we can start the upload
    // rather than streaming it. S3 has an alternate API for doing
    // chunked uploads, but (a) it has a minimum chunk size of 5 MB, so
    // it doesn't help us much (many/most stars will be smaller than
    // that), and (b) it's nonstandard, so we'd have to bake in S3's
    // specific scheme. Doesn't seem worthwhile for now, so just tar to
    // a temporary directory. If stars get radically bigger then it
    // might be worthwhile to tar to memory and spill to S3 every 5MB.
    starball = path.join(tmpdir, topLevelDirName + ".tar.gz");
    files.createTarball(bundlePath, starball);
  } else {
    starball = options.starball;
  }
  process.stdout.write('Uploading...\n');

  var galaxy = getGalaxy(options.context);
  var Package = getPackage(options.context);

  var created = true;
  var appConfig = {
      settings: options.settings
  };

  if (options.admin)
    appConfig.admin = true;

  try {
    galaxy.call('createApp', options.app, appConfig);
  } catch (e) {
    if (e instanceof Package.meteor.Meteor.Error && e.error === 'already-exists') {
      // Cool, it already exists. No problem. Just set the settings if they were
      // passed. We explicitly check for undefined because we want to allow you
      // to unset settings by passing an empty file.
      if (options.settings !== undefined) {
        try {
          galaxy.call('updateAppConfiguration', options.app, appConfig);
        } catch (e) {
          exitWithError(e);
        }
      }
      created = false;
    } else {
      exitWithError(e);
    }
  }

  // Get the upload information from Galaxy. It's a surprise if this
  // fails (we already know the app exists.)
  var info = prettyCall(galaxy, 'beginUploadStar', [options.app, bundleResult.starManifest]);

  // Upload
  // XXX copied from galaxy/tool/galaxy.js
  var fileSize = fs.statSync(starball).size;
  var fileStream = fs.createReadStream(starball);
  var future = new Future;
  var req = httpHelpers.request({
    method: "PUT",
    url: info.put,
    headers: { 'content-length': fileSize,
               'content-type': 'application/octet-stream' },
    strictSSL: true
  }, function (error, response, body) {
    if (error || ((response.statusCode !== 200)
                  && (response.statusCode !== 201))) {
      if (error && error.message)
        process.stderr.write("Upload failed: " + error.message + "\n");
      else
        process.stderr.write("Upload failed" +
                             (response.statusCode ?
                              " (" + response.statusCode + ")\n" : "\n"));
      process.exit(1);
    }
    future.return();
  });

  fileStream.pipe(req);
  future.wait();

  var result = prettyCall(galaxy, 'completeUploadStar', [info.id], {
    'no-such-upload': 'Upload request expired. Try again.'
  });

  if (created)
    process.stderr.write(options.app + ": created app\n");

  process.stderr.write(options.app + ": " +
                       "pushed revision " + result.serial + "\n");
  // Close the connection to Galaxy (otherwise Node will continue running).
  galaxy.close();
};

// options:
// - context
// - app
// - streaming (BOOL)
exports.logs = function (options) {
  var logReaderURL;
  if (options.context.galaxy.adminBaseUrl) {
    logReaderURL = options.context.galaxy.adminBaseUrl + "log-reader";
  } else {
    var galaxy = getGalaxy(options.context);
    logReaderURL = prettyCall(galaxy, "getLogReaderURL", [], {
      'no-log-reader': "Can't find log reader service"
    });
    galaxy.close();
  }

  var lastLogId = null;
  var logReader =
        getPackage(options.context).livedata.DDP.connect(logReaderURL);
  var Log = unipackage.load({
    library: options.context.library,
    packages: [ 'logging' ],
    release: options.context.releaseVersion
  }).logging.Log;

  var ok = logReader.registerStore('logs', {
    update: function (msg) {
      // Ignore all messages but 'changed'
      if (msg.msg !== 'changed')
        return;
      var obj = msg.fields.obj;
      lastLogId = msg.fields.id;
      obj = Log.parse(obj);
      obj && console.log(Log.format(obj, {color: true}));
    }
  });

  if (!ok)
    throw new Error("Can't listen to messages on the logs collection");

  var logsSubscription = null;
  // In case of reconnect recover the state so user sees only new logs
  logReader.onReconnect = function () {
    logsSubscription && logsSubscription.stop();
    var opts = { streaming: options.streaming };
    if (lastLogId)
      opts.resumeAfterId = lastLogId;
    logsSubscription = logReader.subscribe("logsForApp", options.app, opts);
  };
  logsSubscription = prettySub(logReader, "logsForApp",
                               [options.app, {streaming: options.streaming}],
                               {"no-such-app": "No such app: " + options.app});

  // if streaming is needed there is no point in closing connection
  if (!options.streaming) {
    // Close connections to Galaxy and log-reader
    // (otherwise Node will continue running).
    logReader.close();
  }
};

// options:
// - context
// - app
exports.temporaryMongoUrl = function (options) {
  var galaxy = getGalaxy(options.context);
  var url = galaxy.call('getTemporaryMongoUrl', options.app);
  galaxy.close();
  return url;
};


Package.describe({
  summary: "Tiny testing framework",
  internal: true
});

Package.on_use(function (api) {
  api.use('underscore', ['client', 'server']);
  api.use('random', ['client', 'server']);

  api.export('Tinytest');

  api.add_files('tinytest.js', ['client', 'server']);

  api.use('livedata', ['client', 'server']);
  api.use('mongo-livedata', ['client', 'server']);
  api.add_files('model.js', ['client', 'server']);

  api.add_files('tinytest_client.js', 'client');
  api.add_files('tinytest_server.js', 'server');

  api.use('check');
});


// Remaining to update:
// - $exists
// - $type
// - $regex/$option
// - $all


// XXX We should revisit how we factor MongoDB support into (1) the
// server-side node.js driver [which you might use independently of
// livedata, after all], (2) minimongo [ditto], and (3) Collection,
// which is the class that glues the two of them to Livedata, but also
// is generally the "public interface for newbies" to Mongo in the
// Meteor universe. We want to allow the components to be used
// independently, but we don't want to overwhelm the user with
// minutiae.

Package.describe({
  summary: "Adaptor for using MongoDB and Minimongo over DDP",
  internal: true
});

Npm.depends({
  // 1.3.19, plus a patch to add oplogReplay flag:
  // https://github.com/mongodb/node-mongodb-native/pull/1108
  mongodb: "https://github.com/meteor/node-mongodb-native/tarball/779bbac916a751f305d84c727a6cc7dfddab7924"
});

Package.on_use(function (api) {
  api.use(['random', 'ejson', 'json', 'underscore', 'minimongo', 'logging',
           'livedata', 'deps', 'application-configuration'],
          ['client', 'server']);
  api.use('check', ['client', 'server']);

  // Allow us to detect 'insecure'.
  api.use('insecure', {weak: true});

  // Allow us to detect 'autopublish', and publish collections if it's loaded.
  api.use('autopublish', 'server', {weak: true});

  // Allow us to detect 'disable-oplog', which turns off oplog tailing for your
  // app even if it's configured in the environment. (This package will be
  // probably be removed before 1.0.)
  api.use('disable-oplog', 'server', {weak: true});

  // defaultRemoteCollectionDriver gets its deployConfig from something that is
  // (for questionable reasons) initialized by the webapp package.
  api.use('webapp', 'server', {weak: true});

  // If the facts package is loaded, publish some statistics.
  api.use('facts', 'server', {weak: true});

  // Stuff that should be exposed via a real API, but we haven't yet.
  api.export('MongoInternals', 'server');
  // For tests only.
  api.export('MongoTest', 'server', {testOnly: true});

  api.add_files(['mongo_driver.js', 'oplog_tailing.js',
                 'observe_multiplex.js', 'doc_fetcher.js',
                 'polling_observe_driver.js','oplog_observe_driver.js'],
                'server');
  api.add_files('local_collection_driver.js', ['client', 'server']);
  api.add_files('remote_collection_driver.js', 'server');
  api.add_files('collection.js', ['client', 'server']);
});

Package.on_test(function (api) {
  api.use('mongo-livedata');
  api.use('check');
  api.use(['tinytest', 'underscore', 'test-helpers', 'ejson', 'random',
           'livedata']);
  // XXX test order dependency: the allow_tests "partial allow" test
  // fails if it is run before mongo_livedata_tests.
  api.add_files('mongo_livedata_tests.js', ['client', 'server']);
  api.add_files('allow_tests.js', ['client', 'server']);
  api.add_files('collection_tests.js', ['client', 'server']);
  api.add_files('observe_changes_tests.js', ['client', 'server']);
  api.add_files('oplog_tests.js', 'server');
  api.add_files('doc_fetcher_tests.js', 'server');
});


// Meteor Universal Events -- Normalized cross-browser event handling library
//
// This module lets you set up a function f that will be called
// whenever an event fires on any element in the DOM. Specifically,
// when an event fires on node N, f will be called with N. Then, if
// the event is a bubbling event, f will be called again with N's
// parent, then called again with N's grandparent, etc, until the root
// of the document is reached. This provides a good base on top of
// which custom event handling systems can be implemented.
//
// f also receives the event object for the event that fired. The
// event object is normalized and extended to smooth over
// cross-browser differences in event handling. See the details in
// setHandler.
//
// Usage:
//   var listener = new UniversalEventListener(function (event) { ... });
//   listener.addType("click");
//
// If you want to support IE <= 8, you must also call installHandler
// on each subtree of DOM nodes on which you wish to receive events,
// eg, before inserting them into the document.
//
// Universal Events works reliably for events that fire on any DOM
// element. It may not work consistently across browsers for events
// that are intended to fire on non-element nodes (eg, text nodes).
// We're not sure if it's possible to handle those events consistently
// across browsers, but in any event, it's not a common use case.
//
// Implementation notes:
//
// Internally, there are two separate implementations, one for modern
// browsers (in events-w3c.js), and one for old browsers with no
// event capturing support (in events-ie.js.) The correct
// implementation will be chosen for you automatically at runtime.

var listeners = [];

var returnFalse = function() { return false; };
var returnTrue = function() { return true; };

// inspired by jquery fix()
var normalizeEvent = function (event) {
  var originalStopPropagation = event.stopPropagation;
  var originalPreventDefault = event.preventDefault;
  event.isPropagationStopped = returnFalse;
  event.isImmediatePropagationStopped = returnFalse;
  event.isDefaultPrevented = returnFalse;
  event.stopPropagation = function() {
    event.isPropagationStopped = returnTrue;
    if (originalStopPropagation)
      originalStopPropagation.call(event);
    else
      event.cancelBubble = true; // IE
  };
  event.preventDefault = function() {
    event.isDefaultPrevented = returnTrue;
    if (originalPreventDefault)
      originalPreventDefault.call(event);
    else
      event.returnValue = false; // IE
  };
  event.stopImmediatePropagation = function() {
    event.stopPropagation();
    event.isImmediatePropagationStopped = returnTrue;
  };

  var type = event.type;

  // adapted from jquery
  if (event.metaKey === undefined)
    event.metaKey = event.ctrlKey;
  if (/^key/.test(type)) {
    // KEY EVENTS
    // Add which.  Technically char codes and key codes are
    // different things; the former is ASCII/unicode/etc and the
    // latter is arbitrary.  But browsers that lack charCode
    // seem to put character info in keyCode.
    // (foo == null) tests for null or undefined
    if (event.which == null)
      event.which = (event.charCode != null ? event.charCode : event.keyCode);
  } else if (/^(?:mouse|contextmenu)|click/.test(type)) {
    // MOUSE EVENTS
    // Add relatedTarget, if necessary
    if (! event.relatedTarget && event.fromElement)
      event.relatedTarget = (event.fromElement === event.target ?
                             event.toElement : event.fromElement);
    // Add which for click: 1 === left; 2 === middle; 3 === right
    if (! event.which && event.button !== undefined ) {
      var button = event.button;
      event.which = (button & 1 ? 1 :
                     (button & 2 ? 3 :
                       (button & 4 ? 2 : 0 )));
    }
  }

  return event;
};

var deliver = function (event) {
  event = normalizeEvent(event);
  _.each(listeners, function (listener) {
    if (listener.types[event.type]) {
      // if in debug mode, filter out events where the user forgot
      // to call installHandler, even if we're not on IE
      if (!(listener._checkIECompliance &&
            ! event.currentTarget['_uevents_test_eventtype_' + event.type]))
        listener.handler.call(null, event);
    }
  });
};

// When IE8 is dead, we can remove this springboard logic.
var impl;
var getImpl = function () {
  if (! impl)
    impl = (document.addEventListener ?
            new UniversalEventListener._impl.w3c(deliver) :
            new UniversalEventListener._impl.ie(deliver));
  return impl;
};

var typeCounts = {};

////////// PUBLIC API

// Create a new universal event listener with a given handler.
// Until some event types are turned on with `addType`, the handler
// will not receive any events.
//
// Whenever an event of the appropriate type fires anywhere in the
// document, `handler` will be called with one argument, the
// event. If the event is a bubbling event (most events are
// bubbling, eg, 'click'), then `handler` will be called not only
// for the element that was the origin of the event (eg, the button
// that was clicked), but for each parent element as the event
// bubbles up to the top of the tree.
//
// The event object that's passed to `handler` will be normalized
// across browsers so that it contains the following fields and
// methods:
//
// - type (e.g. "click")
// - target
// - currentTarget
// - stopPropagation()
// - preventDefault()
// - isPropagationStopped()
// - isDefaultPrevented()
//
// NOTE: If you want compatibility with IE <= 8, you will need to
// call `installHandler` to prepare each subtree of the DOM to receive
// the events you are interested in.
//
// Debugging only:
//
// The _checkIECompliance flag enables extra checking that the user
// is correctly registering new DOM nodes with installHandler, even
// in browsers that don't require it. In other words, when the flag
// is set, modern browsers will require the same API calls as IE <=
// 8. This is only used for tests and is private for now.
UniversalEventListener = function (handler, _checkIECompliance) {
  this.handler = handler;
  this.types = {}; // map from event type name to 'true'
  this.impl = getImpl();
  this._checkIECompliance = _checkIECompliance;
  listeners.push(this);
};

_.extend(UniversalEventListener.prototype, {
  // Adds `type` to the set of event types that this listener will
  // listen to and deliver to the handler.  Has no effect if `type`
  // is already in the set.
  addType: function (type) {
    if (!this.types[type]) {
      this.types[type] = true;
      typeCounts[type] = (typeCounts[type] || 0) + 1;
      if (typeCounts[type] === 1)
        this.impl.addType(type);
    }
  },

  // Removes `type` from the set of event types that this listener
  // will listen to and deliver to the handler.  Has no effect if `type`
  // is not in the set.
  removeType: function (type) {
    if (this.types[type]) {
      delete this.types[type];
      typeCounts[type]--;
      if (! typeCounts[type])
        this.impl.removeType(type);
    }
  },

  // It is only necessary to call this method if you want to support
  // IE <= 8. On those browsers, you must call this method on each
  // set of nodes before adding them to the DOM (or at least, before
  // expecting to receive events on them), and you must specify the
  // types of events you'll be receiving.
  //
  // `node` and all of its descendents will be set up to handle
  // events of type `type` (eg, 'click'). Only current descendents
  // of `node` are affected; if new nodes are added to the subtree
  // later, installHandler must be called again to ensure events are
  // received on those nodes. To set up to handle multiple event
  // types, make multiple calls.
  //
  // It is safe to call installHandler any number of times on the same
  // arguments (it is idempotent).
  //
  // If you forget to call this function for a given node, it's
  // unspecified whether you'll receive events on IE <= 8 (you may,
  // you may not.) If you don't care about supporting IE <= 8 you
  // can ignore this function.
  installHandler: function (node, type) {
    // Only work on element nodes, not e.g. text nodes or fragments
    if (node.nodeType !== 1)
      return;
    this.impl.installHandler(node, type);

    // When in checkIECompliance mode, mark all the nodes in the current subtree.
    // We will later block events on nodes that weren't marked.  This
    // tests that Spark is generating calls to registerEventType
    // with proper subtree information, even in browsers that don't need
    // it.
    if (this._checkIECompliance) {
      // set flag to mark the node for this type, recording the
      // fact that installHandler was called for this node and type.
      // the property value can be any non-primitive value (to prevent
      // showing up as an HTML attribute in IE) so we use `node` itself.
      node['_uevents_test_eventtype_'+type] = node;
      if (node.firstChild) {
        _.each(node.getElementsByTagName('*'), function(x) {
          x['_uevents_test_eventtype_'+type] = x;
        });
      }
    }
  },

  // Tear down this UniversalEventListener so that no more events
  // are delivered.
  destroy: function () {
    var self = this;

    listeners = _.without(listeners, self);
    _.each(_.keys(self.types), function (type) {
      self.removeType(type);
    });
  }
});


{"name":"sprockets-rails","number":"1.0.0","platform":"ruby","dependencies":[["sprockets","~> 2.3.1"],["railties","< 5.0, >= 4.0.0.beta"]]},

keep keep keep keep keep replace replace keep keep keep

var semver = Npm.require('semver');

// Setup mock data for tests
var Packages = new LocalCollection;
var Versions = new LocalCollection;
var Builds = new LocalCollection;

Packages.insert({ name: "sparky-forms" });
Packages.insert({ name: "forms" });
Packages.insert({ name: "sparkle" });
Packages.insert({ name: "awesome-dropdown" });
Packages.insert({ name: "dropdown" });
Packages.insert({ name: "jquery-widgets" });
Packages.insert({ name: "jquery" });

var insertVersion = function (name, version, ecv, deps) {
  var constructedDeps = {};
  _.each(deps, function (constraint, name) {
    constructedDeps[name] = {
      constraint: constraint,
      references: [
        { slice: "main", arch: "os", targetSlice: "main", weak: false,
          implied: false, unordered: false },
        { slice: "main", arch: "browser", targetSlice: "main", weak: false,
          implied: false, unordered: false }]
    };
  });
  Versions.insert({ packageName: name, version: version, earliestCompatibleVersion: ecv,
                    dependencies: constructedDeps });
};
insertVersion("sparky-forms", "1.1.2", "1.0.0", {"forms": "=1.0.1", "sparkle": "=2.1.1"});
insertVersion("forms", "1.0.1", "1.0.0", {"sparkle": "2.1.0", "jquery-widgets": "1.0.0"});
insertVersion("sparkle", "2.1.0", "2.1.0", {"jquery": "1.8.2"});
insertVersion("sparkle", "2.1.1", "2.1.0", {"jquery": "1.8.2"});
insertVersion("sparkle", "1.0.0", "1.0.0");
insertVersion("awesome-dropdown", "1.5.0", "1.0.0", {"dropdown": "=1.2.2"});
insertVersion("dropdown", "1.2.2", "1.0.0", {"jquery-widgets": "1.0.0"});
insertVersion("jquery-widgets", "1.0.0", "1.0.0", {"jquery": "1.8.0", "sparkle": "2.1.1"});
insertVersion("jquery-widgets", "1.0.2", "1.0.0", {"jquery": "1.8.0", "sparkle": "2.1.1"});
insertVersion("jquery", "1.8.0", "1.8.0");
insertVersion("jquery", "1.8.2", "1.8.0");

var insertBuild = function (name, version, ecv) {
  Builds.insert({ packageName: name, version: version,
                  earliestCompatibleVersion: ecv, architecture: [ "browser", "os" ] });
};

insertBuild("sparky-forms", "1.1.2", "1.1.0");
insertBuild("forms", "1.0.1", "1.0.0");
insertBuild("sparkle", "2.1.1", "2.1.0");
insertBuild("sparkle", "2.1.0", "2.1.0");
insertBuild("sparkle", "1.0.0", "1.0.0");
insertBuild("awesome-dropdown", "1.5.0", "1.0.0");
insertBuild("dropdown", "1.2.2", "1.0.0");
insertBuild("jquery-widgets", "1.0.0", "1.0.0");
insertBuild("jquery-widgets", "1.0.2", "1.0.0");
insertBuild("jquery", "1.8.0", "1.0.0");
insertBuild("jquery", "1.9.0", "1.0.0");

// XXX Temporary hack: make a catalog stub to pass in to the constraint
// solver. We'll soon move constraint-solver into tools and just run
// tests with self-test, passing a real Catalog object.
var catalogStub = {
  packages: Packages,
  versions: Versions,
  builds: Builds,
  getAllPackageNames: function () {
    return _.pluck(Packages.find().fetch(), 'name');
  },
  getPackage: function (name) {
    return this.packages.findOne({ name: name });
  },
  getSortedVersions: function (name) {
    return _.pluck(
      this.versions.find({
        packageName: name
      }, { fields: { version: 1 } }).fetch(),
      'version'
    ).sort(semver.compare);
  },
  getVersion: function (name, version) {
    return this.versions.findOne({
      packageName: name,
      version: version
    });
  }
};

var resolver = new ConstraintSolver.PackagesResolver(catalogStub);

var currentTest = null;
var t = function (deps, expected, options) {
  var resolvedDeps = resolver.resolve(deps, options);
  currentTest.equal(resolvedDeps, expected);
};

var t_progagateExact = function (deps, expected) {
  var resolvedDeps = resolver.propagatedExactDeps(deps);
  currentTest.equal(resolvedDeps, expected);
};

var FAIL = function (deps) {
  currentTest.throws(function () {
    var resolvedDeps = resolver.resolve(deps);
  });
};

Tinytest.add("constraint solver - exact dependencies", function (test) {
  currentTest = test;
  t_progagateExact({ "sparky-forms": "=1.1.2" }, { "sparky-forms": "1.1.2", "forms": "1.0.1", "sparkle": "2.1.1" });
  t_progagateExact({ "sparky-forms": "=1.1.2", "forms": "=1.0.1" }, { "sparky-forms": "1.1.2", "forms": "1.0.1", "sparkle": "2.1.1" });
  t_progagateExact({ "sparky-forms": "=1.1.2", "sparkle": "=2.1.1" }, { "sparky-forms": "1.1.2", "forms": "1.0.1", "sparkle": "2.1.1" });
  t_progagateExact({ "awesome-dropdown": "=1.5.0" }, { "awesome-dropdown": "1.5.0", "dropdown": "1.2.2" });

  FAIL({ "sparky-forms": "=1.1.2", "sparkle": "=1.0.0" });
  // something that isn't available for your architecture
  FAIL({ "sparky-forms": "=1.1.2", "sparkle": "=2.0.0" });
  FAIL({ "sparky-forms": "=0.0.1" });
});

Tinytest.add("constraint solver - simple exact + regular deps", function (test) {
  currentTest = test;

  t({ "sparky-forms": "=1.1.2" }, {
    "sparky-forms": "1.1.2",
    "forms": "1.0.1",
    "sparkle": "2.1.1",
    "jquery-widgets": "1.0.0",
    "jquery": "1.8.2"
  }, { mode: "CONSERVATIVE" });

  t({ "sparky-forms": "=1.1.2", "awesome-dropdown": "=1.5.0" }, {
    "sparky-forms": "1.1.2",
    "forms": "1.0.1",
    "sparkle": "2.1.1",
    "jquery-widgets": "1.0.0",
    "jquery": "1.8.2",
    "awesome-dropdown": "1.5.0",
    "dropdown": "1.2.2"
  }, { mode: "CONSERVATIVE" });
});

Tinytest.add("constraint solver - non-exact direct dependency", function (test) {
  currentTest = test;
  // sparky-forms 1.0.0 won't be chosen because it depends on a very old
  // jquery, which is not compatible with the jquery that
  // awesome-dropdown uses.
  t({ "sparky-forms": "1.0.0", "awesome-dropdown": "=1.5.0" }, {
    "sparky-forms": "1.1.2",
    "forms": "1.0.1",
    "sparkle": "2.1.1",
    "jquery-widgets": "1.0.0",
    "jquery": "1.8.2",
    "awesome-dropdown": "1.5.0",
    "dropdown": "1.2.2"
  }, { mode: "CONSERVATIVE" });
});

Tinytest.add("constraint solver - no constraint dependency - anything", function (test) {
  currentTest = test;
  var versions = resolver.resolve({ "sparkle": "none" }, { mode: "CONSERVATIVE" });
  test.isTrue(_.isString(versions.sparkle));
  versions = resolver.resolve({ "sparkle": null }, { mode: "CONSERVATIVE" });
  test.isTrue(_.isString(versions.sparkle));
});

Tinytest.add("constraint solver - no constraint dependency - transitive dep still picked right", function (test) {
  currentTest = test;
  var versions = resolver.resolve({ "sparkle": "none", "sparky-forms": "1.1.2" }, { mode: "CONSERVATIVE" });
  test.equal(versions.sparkle, "2.1.1");
  var versions = resolver.resolve({ "sparkle": null, "sparky-forms": "1.1.2" }, { mode: "CONSERVATIVE" });
  test.equal(versions.sparkle, "2.1.1");
});

Tinytest.add("constraint solver - benchmark on gems", function (test) {
  var r = new ConstraintSolver.PackagesResolver(getCatalogStub(sinatraGems));

  r.resolve({
    'capistrano': '2.14.2',
    'data_mapper': '1.2.0',
    'dm-core': '1.2.0',
    'dm-sqlite-adapter': '1.2.0',
    'dm-timestamps': '1.2.0',
    'haml': '3.1.7',
    'sass': '3.2.1',
    'shotgun': '0.9.0',
    'sinatra': '1.3.5',
    'sqlite3': '1.3.7'
  });
});

// Given a set of gems definitions returns a Catalog-like object
function getCatalogStub (gems) {
  return {
    getAllPackageNames: function () {
      return _.uniq(_.pluck(gems, 'name'));
    },
    getPackage: function (name) {
      throw new Error("Not implemeneted");
    },
    getSortedVersions: function (name) {
      return _.chain(gems)
        .filter(function (pv) { return pv.name === name; })
        .pluck('number')
        .map(function (version) {
          var nv = exactVersion(version);
          if (nv.length < version.length)
            return version;
          return nv;
        })
        .filter(function (v) {
          return semver.valid(v);
        })
        .value()
        .sort(semver.compare);
    },
    getVersion: function (name, version) {
      var gem = _.find(gems, function (pv) {
        return pv.name === name && exactVersion(pv.number) === version;
      });

      var ecv = function (version) {
        // hard-code to "x.0.0"
        return parseInt(version) + ".0.0";
      };

      var packageVersion = {
        packageName: gem.name,
        version: gem.number,
        earliestCompatibleVersion: ecv(gem.number),
        dependencies: {}
      };

      if (packageVersion.packageName === "rack")
        packageVersion.earliestCompatibleVersion = "0.0.0";

      _.each(gem.dependencies, function (dep) {
        var name = dep[0];
        var constraints = dep[1];

        packageVersion.dependencies[name] = {
          constraint: convertConstraints(constraints)[0], // XXX pick first one only
          references: [{
            "slice": "main",
            "arch": "browser"
          }, {
            "slice": "main",
            "arch": "os" }]
        };
      });

      return packageVersion;
    }
  };
}

// Naively converts ruby-gems style constraints string to either exact
// constraint or a regular constraint.
function convertConstraints (inp) {
  var out = inp.split(",").map(function (s) {
    return s.trim();
  })
  // remove the constraints we don't support
  .filter(function (s) {
    return !/</g.test(s) && !/!=/.test(s);
  })
  // convert 1.2.3.beta2 => 1.2.3
  // and 0.2 => 0.2.0
  .map(function (s) {
    var x = s.split(" ");
    return [x[0], exactVersion(x[1])].join(" ");
  })
  // convert '= 1.2.3' => '=1.2.3'
  // '~>1.2.3' => '1.2.3'
  // '>=1.2.3' => '1.2.3'
  .map(function (s) {
    var x = s.split(' ');
    if (x[0] === '~>' || x[0] === '>=' || x[0] === '>')
      x[0] = '';
    else if (x[0] === '=')
      x[0] = '=';
    else
      throw new Error('unknown operator: ' + x[0]);
    return x.join("");
  });

  return out;
}

function exactVersion (s) {
  s = s.match(/\d+(\.\d+(\.\d+)?)?/)[0];
  if (s.split('.').length < 3)
    s += ".0";
  if (s.split('.').length < 3)
    s += ".0";
  return s;
}


// XXX it'd be cool to also test that the right thing happens if options
// *are* validated, but Accounts._options is global state which makes this hard
// (impossible?)
Tinytest.add('accounts - config validates keys', function (test) {
  test.throws(function () {
    Accounts.config({foo: "bar"});
  });
});

var idsInValidateNewUser = {};
Accounts.validateNewUser(function (user) {
  idsInValidateNewUser[user._id] = true;
  return true;
});

Tinytest.add('accounts - validateNewUser gets passed user with _id', function (test) {
  var newUserId = Accounts.updateOrCreateUserFromExternalService('foobook', {id: Random.id()}).id;
  test.isTrue(newUserId in idsInValidateNewUser);
});

Tinytest.add('accounts - updateOrCreateUserFromExternalService - Facebook', function (test) {
  var facebookId = Random.id();

  // create an account with facebook
  var uid1 = Accounts.updateOrCreateUserFromExternalService(
    'facebook', {id: facebookId, monkey: 42}, {profile: {foo: 1}}).id;
  var users = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
  test.length(users, 1);
  test.equal(users[0].profile.foo, 1);
  test.equal(users[0].services.facebook.monkey, 42);

  // create again with the same id, see that we get the same user.
  // it should update services.facebook but not profile.
  var uid2 = Accounts.updateOrCreateUserFromExternalService(
    'facebook', {id: facebookId, llama: 50},
    {profile: {foo: 1000, bar: 2}}).id;
  test.equal(uid1, uid2);
  users = Meteor.users.find({"services.facebook.id": facebookId}).fetch();
  test.length(users, 1);
  test.equal(users[0].profile.foo, 1);
  test.equal(users[0].profile.bar, undefined);
  test.equal(users[0].services.facebook.llama, 50);
  // make sure we *don't* lose values not passed this call to
  // updateOrCreateUserFromExternalService
  test.equal(users[0].services.facebook.monkey, 42);

  // cleanup
  Meteor.users.remove(uid1);
});

Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', function (test) {
  var weiboId1 = Random.id();
  var weiboId2 = Random.id();

  // users that have different service ids get different users
  var uid1 = Accounts.updateOrCreateUserFromExternalService(
    'weibo', {id: weiboId1}, {profile: {foo: 1}}).id;
  var uid2 = Accounts.updateOrCreateUserFromExternalService(
    'weibo', {id: weiboId2}, {profile: {bar: 2}}).id;
  test.equal(Meteor.users.find({"services.weibo.id": {$in: [weiboId1, weiboId2]}}).count(), 2);
  test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).profile.foo, 1);
  test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).emails, undefined);
  test.equal(Meteor.users.findOne({"services.weibo.id": weiboId2}).profile.bar, 2);
  test.equal(Meteor.users.findOne({"services.weibo.id": weiboId2}).emails, undefined);

  // cleanup
  Meteor.users.remove(uid1);
  Meteor.users.remove(uid2);
});

Tinytest.add('accounts - updateOrCreateUserFromExternalService - Twitter', function (test) {
  var twitterIdOld = parseInt(Random.hexString(4), 16);
  var twitterIdNew = ''+twitterIdOld;

  // create an account with twitter using the old ID format of integer
  var uid1 = Accounts.updateOrCreateUserFromExternalService(
    'twitter', {id: twitterIdOld, monkey: 42}, {profile: {foo: 1}}).id;
  var users = Meteor.users.find({"services.twitter.id": twitterIdOld}).fetch();
  test.length(users, 1);
  test.equal(users[0].profile.foo, 1);
  test.equal(users[0].services.twitter.monkey, 42);

  // Update the account with the new ID format of string
  // test that the existing user is found, and that the ID
  // gets updated to a string value
  var uid2 = Accounts.updateOrCreateUserFromExternalService(
    'twitter', {id: twitterIdNew, monkey: 42}, {profile: {foo: 1}}).id;
  test.equal(uid1, uid2);
  users = Meteor.users.find({"services.twitter.id": twitterIdNew}).fetch();
  test.length(users, 1);

  // cleanup
  Meteor.users.remove(uid1);
});

Tinytest.add('accounts - insertUserDoc username', function (test) {
  var userIn = {
    username: Random.id()
  };

  // user does not already exist. create a user object with fields set.
  var result = Accounts.insertUserDoc(
    {profile: {name: 'Foo Bar'}},
    userIn
  );
  var userOut = Meteor.users.findOne(result.id);

  test.equal(typeof userOut.createdAt, 'object');
  test.equal(userOut.profile.name, 'Foo Bar');
  test.equal(userOut.username, userIn.username);

  // run the hook again. now the user exists, so it throws an error.
  test.throws(function () {
    Accounts.insertUserDoc(
      {profile: {name: 'Foo Bar'}},
      userIn
    );
  });

  // cleanup
  Meteor.users.remove(result.id);

});

Tinytest.add('accounts - insertUserDoc email', function (test) {
  var email1 = Random.id();
  var email2 = Random.id();
  var email3 = Random.id();
  var userIn = {
    emails: [{address: email1, verified: false},
             {address: email2, verified: true}]
  };

  // user does not already exist. create a user object with fields set.
  var result = Accounts.insertUserDoc(
    {profile: {name: 'Foo Bar'}},
    userIn
  );
  var userOut = Meteor.users.findOne(result.id);

  test.equal(typeof userOut.createdAt, 'object');
  test.equal(userOut.profile.name, 'Foo Bar');
  test.equal(userOut.emails, userIn.emails);

  // run the hook again with the exact same emails.
  // run the hook again. now the user exists, so it throws an error.
  test.throws(function () {
    Accounts.insertUserDoc(
      {profile: {name: 'Foo Bar'}},
      userIn
    );
  });

  // now with only one of them.
  test.throws(function () {
    Accounts.insertUserDoc(
      {}, {emails: [{address: email1}]}
    );
  });

  test.throws(function () {
    Accounts.insertUserDoc(
      {}, {emails: [{address: email2}]}
    );
  });

  // a third email works.
  var result3 = Accounts.insertUserDoc(
      {}, {emails: [{address: email3}]}
  );
  var user3 = Meteor.users.findOne(result3.id);
  test.equal(typeof user3.createdAt, 'object');

  // cleanup
  Meteor.users.remove(result.id);
  Meteor.users.remove(result3.id);
});

// More token expiration tests are in accounts-password
Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete) {
  var userIn = { username: Random.id() };
  var result = Accounts.insertUserDoc({ profile: {
    name: 'Foo Bar'
  } }, userIn);
  var date = new Date(new Date() - 5000);
  Meteor.users.update(result.id, {
    $set: {
      "services.resume.loginTokens": [{
        hashedToken: Random.id(),
        when: date
      }, {
        hashedToken: Random.id(),
        when: +date
      }]
    }
  });
  var observe = Meteor.users.find(result.id).observe({
    changed: function (newUser) {
      if (newUser.services && newUser.services.resume &&
          _.isEmpty(newUser.services.resume.loginTokens)) {
        observe.stop();
        onComplete();
      }
    }
  });
  Accounts._expireTokens(new Date(), result.id);
});

// Login tokens used to be stored unhashed in the database.  We want
// to make sure users can still login after upgrading.

var userWithUnhashedLoginToken = function () {
  var result = Accounts.insertUserDoc({}, {username: Random.id()});

  // Construct an old-style unhashed login token.
  var stampedToken = Accounts._generateStampedLoginToken();

  Meteor.users.update(
    result.id,
    {$push: {'services.resume.loginTokens': stampedToken}}
  );

  // Return result, as if from Accounts.insertUserDoc.
  result.token = stampedToken.token;
  result.tokenExpires = Accounts._tokenExpiration(stampedToken.when);
  return result;
};

Tinytest.addAsync('accounts - login token', function (test, onComplete) {
  // Test that we can login when the database contains a leftover
  // old style unhashed login token.
  var user1 = userWithUnhashedLoginToken();
  var connection = DDP.connect(Meteor.absoluteUrl());
  connection.call('login', {resume: user1.token});
  connection.disconnect();

  // Steal the unhashed token from the database and use it to login.
  // This is a sanity check so that when we *can't* login with a
  // stolen *hashed* token, we know it's not a problem with the test.
  var user2 = userWithUnhashedLoginToken();
  var stolenToken = Meteor.users.findOne(user2.id).services.resume.loginTokens[0].token;
  test.isTrue(stolenToken);
  connection = DDP.connect(Meteor.absoluteUrl());
  connection.call('login', {resume: stolenToken});
  connection.disconnect();

  // Now do the same thing, this time with a stolen hashed token.
  var user3 = Accounts.insertUserDoc({generateLoginToken: true}, {username: Random.id()});
  stolenToken = Meteor.users.findOne(user3.id).services.resume.loginTokens[0].hashedToken;
  test.isTrue(stolenToken);
  connection = DDP.connect(Meteor.absoluteUrl());
  // evil plan foiled
  test.throws(
    function () {
      connection.call('login', {resume: stolenToken});
    },
    /You\'ve been logged out by the server/
  );
  connection.disconnect();

  // Old style unhashed tokens are replaced by hashed tokens when
  // encountered.  This means that after someone logins once, the
  // old unhashed token is no longer available to be stolen.
  var user4 = userWithUnhashedLoginToken();
  connection = DDP.connect(Meteor.absoluteUrl());
  connection.call('login', {resume: user4.token});
  connection.disconnect();

  // The token is no longer available to be stolen.
  stolenToken = Meteor.users.findOne(user4.id).services.resume.loginTokens[0].token;
  test.isFalse(stolenToken);

  // After the upgrade, the client can still login with their login token.
  connection = DDP.connect(Meteor.absoluteUrl());
  connection.call('login', {resume: user4.token});
  connection.disconnect();

  onComplete();
});

Tinytest.addAsync(
  'accounts - connection data cleaned up',
  function (test, onComplete) {
    makeTestConnection(
      test,
      function (clientConn, serverConn) {
        // onClose callbacks are called in order, so we run after the
        // close callback in accounts.
        serverConn.onClose(function () {
          test.isFalse(Accounts._getAccountData(serverConn.id, 'connection'));
          onComplete();
        });

        test.isTrue(Accounts._getAccountData(serverConn.id, 'connection'));
        serverConn.close();
      },
      onComplete
    );
  }
);


// Client-side JavaScript, bundled and sent to client.

// Define Minimongo collections to match server/publish.js.
Lists = new Meteor.Collection("lists");
Todos = new Meteor.Collection("todos");

// ID of currently selected list
Session.setDefault('list_id', null);

// Name of currently selected tag for filtering
Session.setDefault('tag_filter', null);

// When adding tag to a todo, ID of the todo
Session.setDefault('editing_addtag', null);

// When editing a list name, ID of the list
Session.setDefault('editing_listname', null);

// When editing todo text, ID of the todo
Session.setDefault('editing_itemname', null);

// Subscribe to 'lists' collection on startup.
// Select a list once data has arrived.
var listsHandle = Meteor.subscribe('lists', function () {
  if (!Session.get('list_id')) {
    var list = Lists.findOne({}, {sort: {name: 1}});
    if (list)
      Router.setList(list._id);
  }
});

var todosHandle = null;
// Always be subscribed to the todos for the selected list.
Deps.autorun(function () {
  var list_id = Session.get('list_id');
  if (list_id)
    todosHandle = Meteor.subscribe('todos', list_id);
  else
    todosHandle = null;
});

////////// Helpers for in-place editing //////////

// Returns an event map that handles the "escape" and "return" keys and
// "blur" events on a text input (given by selector) and interprets them
// as "ok" or "cancel".
var okCancelEvents = function (selector, callbacks) {
  var ok = callbacks.ok || function () {};
  var cancel = callbacks.cancel || function () {};

  var events = {};
  events['keyup '+selector+', keydown '+selector+', focusout '+selector] =
    function (evt) {
      if (evt.type === "keydown" && evt.which === 27) {
        // escape = cancel
        cancel.call(this, evt);

      } else if (evt.type === "keyup" && evt.which === 13 ||
                 evt.type === "focusout") {
        // blur/return/enter = ok/submit if non-empty
        var value = String(evt.target.value || "");
        if (value)
          ok.call(this, value, evt);
        else
          cancel.call(this, evt);
      }
    };

  return events;
};

var activateInput = function (input) {
  input.focus();
  input.select();
};

////////// Lists //////////

Template.lists.loading = function () {
  return !listsHandle.ready();
};

Template.lists.lists = function () {
  return Lists.find({}, {sort: {name: 1}});
};

Template.lists.events({
  'mousedown .list': function (evt) { // select list
    Router.setList(this._id);
  },
  'click .list': function (evt) {
    // prevent clicks on <a> from refreshing the page.
    evt.preventDefault();
  },
  'dblclick .list': function (evt, tmpl) { // start editing list name
    Session.set('editing_listname', this._id);
    Deps.flush(); // force DOM redraw, so we can focus the edit field
    activateInput(tmpl.find("#list-name-input"));
  }
});

// Attach events to keydown, keyup, and blur on "New list" input box.
Template.lists.events(okCancelEvents(
  '#new-list',
  {
    ok: function (text, evt) {
      var id = Lists.insert({name: text});
      Router.setList(id);
      evt.target.value = "";
    }
  }));

Template.lists.events(okCancelEvents(
  '#list-name-input',
  {
    ok: function (value) {
      Lists.update(this._id, {$set: {name: value}});
      Session.set('editing_listname', null);
    },
    cancel: function () {
      Session.set('editing_listname', null);
    }
  }));

Template.lists.selected = function () {
  return Session.equals('list_id', this._id) ? 'selected' : '';
};

Template.lists.name_class = function () {
  return this.name ? '' : 'empty';
};

Template.lists.editing = function () {
  return Session.equals('editing_listname', this._id);
};

////////// Todos //////////

Template.todos.loading = function () {
  return todosHandle && !todosHandle.ready();
};

Template.todos.any_list_selected = function () {
  return !Session.equals('list_id', null);
};

Template.todos.events(okCancelEvents(
  '#new-todo',
  {
    ok: function (text, evt) {
      var tag = Session.get('tag_filter');
      Todos.insert({
        text: text,
        list_id: Session.get('list_id'),
        done: false,
        timestamp: (new Date()).getTime(),
        tags: tag ? [tag] : []
      });
      evt.target.value = '';
    }
  }));

Template.todos.todos = function () {
  // Determine which todos to display in main pane,
  // selected based on list_id and tag_filter.

  var list_id = Session.get('list_id');
  if (!list_id)
    return {};

  var sel = {list_id: list_id};
  var tag_filter = Session.get('tag_filter');
  if (tag_filter)
    sel.tags = tag_filter;

  return Todos.find(sel, {sort: {timestamp: 1}});
};

Template.todo_item.tag_objs = function () {
  var todo_id = this._id;
  return _.map(this.tags || [], function (tag) {
    return {todo_id: todo_id, tag: tag};
  });
};

Template.todo_item.done_class = function () {
  return this.done ? 'done' : '';
};

Template.todo_item.done_checkbox = function () {
  return this.done ? 'checked="checked"' : '';
};

Template.todo_item.editing = function () {
  return Session.equals('editing_itemname', this._id);
};

Template.todo_item.adding_tag = function () {
  return Session.equals('editing_addtag', this._id);
};

Template.todo_item.events({
  'click .check': function () {
    Todos.update(this._id, {$set: {done: !this.done}});
  },

  'click .destroy': function () {
    Todos.remove(this._id);
  },

  'click .addtag': function (evt, tmpl) {
    Session.set('editing_addtag', this._id);
    Deps.flush(); // update DOM before focus
    activateInput(tmpl.find("#edittag-input"));
  },

  'dblclick .display .todo-text': function (evt, tmpl) {
    Session.set('editing_itemname', this._id);
    Deps.flush(); // update DOM before focus
    activateInput(tmpl.find("#todo-input"));
  },

  'click .remove': function (evt) {
    var tag = this.tag;
    var id = this.todo_id;

    evt.target.parentNode.style.opacity = 0;
    // wait for CSS animation to finish
    Meteor.setTimeout(function () {
      Todos.update({_id: id}, {$pull: {tags: tag}});
    }, 300);
  }
});

Template.todo_item.events(okCancelEvents(
  '#todo-input',
  {
    ok: function (value) {
      Todos.update(this._id, {$set: {text: value}});
      Session.set('editing_itemname', null);
    },
    cancel: function () {
      Session.set('editing_itemname', null);
    }
  }));

Template.todo_item.events(okCancelEvents(
  '#edittag-input',
  {
    ok: function (value) {
      Todos.update(this._id, {$addToSet: {tags: value}});
      Session.set('editing_addtag', null);
    },
    cancel: function () {
      Session.set('editing_addtag', null);
    }
  }));

////////// Tag Filter //////////

// Pick out the unique tags from all todos in current list.
Template.tag_filter.tags = function () {
  var tag_infos = [];
  var total_count = 0;

  Todos.find({list_id: Session.get('list_id')}).forEach(function (todo) {
    _.each(todo.tags, function (tag) {
      var tag_info = _.find(tag_infos, function (x) { return x.tag === tag; });
      if (! tag_info)
        tag_infos.push({tag: tag, count: 1});
      else
        tag_info.count++;
    });
    total_count++;
  });

  tag_infos = _.sortBy(tag_infos, function (x) { return x.tag; });
  tag_infos.unshift({tag: null, count: total_count});

  return tag_infos;
};

Template.tag_filter.tag_text = function () {
  return this.tag || "All items";
};

Template.tag_filter.selected = function () {
  return Session.equals('tag_filter', this.tag) ? 'selected' : '';
};

Template.tag_filter.events({
  'mousedown .tag': function () {
    if (Session.equals('tag_filter', this.tag))
      Session.set('tag_filter', null);
    else
      Session.set('tag_filter', this.tag);
  }
});

////////// Tracking selected list in URL //////////

var TodosRouter = Backbone.Router.extend({
  routes: {
    ":list_id": "main"
  },
  main: function (list_id) {
    var oldList = Session.get("list_id");
    if (oldList !== list_id) {
      Session.set("list_id", list_id);
      Session.set("tag_filter", null);
    }
  },
  setList: function (list_id) {
    this.navigate(list_id, true);
  }
});

Router = new TodosRouter;

Meteor.startup(function () {
  Backbone.history.start({pushState: true});
});


keep keep keep replace replace replace replace replace keep keep keep keep


Package.describe({
  summary: "Simulates local storage on IE 6,7 using userData",
  version: "1.0.0",
  internal: true
});

Package.on_use(function (api) {
  api.use('random', 'client');

  api.add_files('localstorage.js', 'client');
});

Package.on_test(function (api) {
  api.use('localstorage', 'client');
  api.use('tinytest');

  api.add_files('localstorage_tests.js', 'client');
});


/**
 * This code does _NOT_ support hot (session-restoring) reloads on
 * IE6,7. It only works on browsers with sessionStorage support.
 *
 * There are a couple approaches to add IE6,7 support:
 *
 * - use IE's "userData" mechanism in combination with window.name.
 * This mostly works, however the problem is that it can not get to the
 * data until after DOMReady. This is a problem for us since this API
 * relies on the data being ready before API users run. We could
 * refactor using Meteor.startup in all API users, but that might slow
 * page loads as we couldn't start the stream until after DOMReady.
 * Here are some resources on this approach:
 * https://github.com/hugeinc/USTORE.js
 * http://thudjs.tumblr.com/post/419577524/localstorage-userdata
 * http://www.javascriptkit.com/javatutors/domstorage2.shtml
 *
 * - POST the data to the server, and have the server send it back on
 * page load. This is nice because it sidesteps all the local storage
 * compatibility issues, however it is kinda tricky. We can use a unique
 * token in the URL, then get rid of it with HTML5 pushstate, but that
 * only works on pushstate browsers.
 *
 * This will all need to be reworked entirely when we add server-side
 * HTML rendering. In that case, the server will need to have access to
 * the client's session to render properly.
 */

// XXX when making this API public, also expose a flag for the app
// developer to know whether a hot code push is happening. This is
// useful for apps using `window.onbeforeunload`. See
// https://github.com/meteor/meteor/pull/657

var KEY_NAME = 'Meteor_Reload';
// after how long should we consider this no longer an automatic
// reload, but a fresh restart. This only happens if a reload is
// interrupted and a user manually restarts things. The only time
// this is really weird is if a user navigates away mid-refresh,
// then manually navigates back to the page.
var TIMEOUT = 30000;

var old_data = {};
// read in old data at startup.
var old_json;
// On Firefox with dom.storage.enabled set to false, sessionStorage is null,
// so we have to both check to see if it is defined and not null.
if (typeof sessionStorage !== "undefined" && sessionStorage) {
  old_json = sessionStorage.getItem(KEY_NAME);
  sessionStorage.removeItem(KEY_NAME);
} else {
  // Unsupported browser (IE 6,7). No session resumption.
  // Meteor._debug("XXX UNSUPPORTED BROWSER");
}

if (!old_json) old_json = '{}';
var old_parsed = {};
try {
  old_parsed = JSON.parse(old_json);
  if (typeof old_parsed !== "object") {
    Meteor._debug("Got bad data on reload. Ignoring.");
    old_parsed = {};
  }
} catch (err) {
  Meteor._debug("Got invalid JSON on reload. Ignoring.");
}

if (old_parsed.reload && typeof old_parsed.data === "object" &&
    old_parsed.time + TIMEOUT > (new Date()).getTime()) {
  // Meteor._debug("Restoring reload data.");
  old_data = old_parsed.data;
}

var providers = [];

////////// External API //////////

Reload = {};

// Packages that support migration should register themselves by
// calling this function. When it's time to migrate, callback will
// be called with one argument, the "retry function." If the package
// is ready to migrate, it should return [true, data], where data is
// its migration data, an arbitrary JSON value (or [true] if it has
// no migration data this time). If the package needs more time
// before it is ready to migrate, it should return false. Then, once
// it is ready to migrating again, it should call the retry
// function. The retry function will return immediately, but will
// schedule the migration to be retried, meaning that every package
// will be polled once again for its migration data. If they are all
// ready this time, then the migration will happen. name must be set if there
// is migration data.
//
Reload._onMigrate = function (name, callback) {
  if (!callback) {
    // name not provided, so first arg is callback.
    callback = name;
    name = undefined;
  }
  providers.push({name: name, callback: callback});
};

// Called by packages when they start up.
// Returns the object that was saved, or undefined if none saved.
//
Reload._migrationData = function (name) {
  return old_data[name];
};

// Migrating reload: reload this page (presumably to pick up a new
// version of the code or assets), but save the program state and
// migrate it over. This function returns immediately. The reload
// will happen at some point in the future once all of the packages
// are ready to migrate.
//
var reloading = false;
Reload._reload = function () {
  if (reloading)
    return;
  reloading = true;

  var tryReload = function () { _.defer(function () {
    // Make sure each package is ready to go, and collect their
    // migration data
    var migrationData = {};
    var remaining = _.clone(providers);
    while (remaining.length) {
      var p = remaining.shift();
      var status = p.callback(tryReload);
      if (!status[0])
        return; // not ready yet..
      if (status.length > 1 && p.name)
        migrationData[p.name] = status[1];
    };

    try {
      // Persist the migration data
      var json = JSON.stringify({
        time: (new Date()).getTime(), data: migrationData, reload: true
      });
    } catch (err) {
      Meteor._debug("Couldn't serialize data for migration", migrationData);
      throw err;
    }

    if (typeof sessionStorage !== "undefined" && sessionStorage) {
      sessionStorage.setItem(KEY_NAME, json);
    } else {
      Meteor._debug("Browser does not support sessionStorage. Not saving migration state.");
    }

    // Tell the browser to shut down this VM and make a new one
    window.location.reload();
  }); };

  tryReload();
};