(function() {
    'use strict';

    // Values have to be sorted - you must have exactly 6 values in each array
    var types = [
        {name: "string", values: ["allan", "bob", "eliot", "joe", "mark", "sara"], keyfield: "k"},
        {name: "double", values: [1.2, 3.5, 4.5, 4.6, 6.7, 9.9], keyfield: "a"},
        {
          name: "date",
          values: [
              new Date(1000000),
              new Date(2000000),
              new Date(3000000),
              new Date(4000000),
              new Date(5000000),
              new Date(6000000)
          ],
          keyfield: "a"
        },
        {
          name: "string_id",
          values: ["allan", "bob", "eliot", "joe", "mark", "sara"],
          keyfield: "_id"
        },
        {
          name: "embedded 1",
          values: ["allan", "bob", "eliot", "joe", "mark", "sara"],
          keyfield: "a.b"
        },
        {
          name: "embedded 2",
          values: ["allan", "bob", "eliot", "joe", "mark", "sara"],
          keyfield: "a.b.c"
        },
        {
          name: "object",
          values: [
              {a: 1, b: 1.2},
              {a: 1, b: 3.5},
              {a: 1, b: 4.5},
              {a: 2, b: 1.2},
              {a: 2, b: 3.5},
              {a: 2, b: 4.5}
          ],
          keyfield: "o"
        },
        {
          name: "compound",
          values: [
              {a: 1, b: 1.2},
              {a: 1, b: 3.5},
              {a: 1, b: 4.5},
              {a: 2, b: 1.2},
              {a: 2, b: 3.5},
              {a: 2, b: 4.5}
          ],
          keyfield: "o",
          compound: true
        },
        {
          name: "oid_id",
          values: [ObjectId(), ObjectId(), ObjectId(), ObjectId(), ObjectId(), ObjectId()],
          keyfield: "_id"
        },
        {
          name: "oid_other",
          values: [ObjectId(), ObjectId(), ObjectId(), ObjectId(), ObjectId(), ObjectId()],
          keyfield: "o"
        },
    ];

    var s = new ShardingTest({name: "key_many", shards: 2});

    assert.commandWorked(s.s0.adminCommand({enableSharding: 'test'}));
    s.ensurePrimaryShard('test', 'shard0001');

    var db = s.getDB('test');
    var primary = s.getServer("test").getDB("test");
    var secondary = s.getOther(primary).getDB("test");

    var curT;

    function makeObjectDotted(v) {
        var o = {};
        if (curT.compound) {
            var prefix = curT.keyfield + '.';
            if (typeof(v) == 'object') {
                for (var key in v)
                    o[prefix + key] = v[key];
            } else {
                for (var key in curT.values[0])
                    o[prefix + key] = v;
            }
        } else {
            o[curT.keyfield] = v;
        }
        return o;
    }

    function makeObject(v) {
        var o = {};
        var p = o;

        var keys = curT.keyfield.split('.');
        for (var i = 0; i < keys.length - 1; i++) {
            p[keys[i]] = {};
            p = p[keys[i]];
        }

        p[keys[i]] = v;

        return o;
    }

    function makeInQuery() {
        if (curT.compound) {
            // cheating a bit...
            return {
                'o.a': {$in: [1, 2]}
            };
        } else {
            return makeObjectDotted({$in: curT.values});
        }
    }

    function getKey(o) {
        var keys = curT.keyfield.split('.');
        for (var i = 0; i < keys.length; i++) {
            o = o[keys[i]];
        }
        return o;
    }

    for (var i = 0; i < types.length; i++) {
        curT = types[i];

        print("\n\n#### Now Testing " + curT.name + " ####\n\n");

        var shortName = "foo_" + curT.name;
        var longName = "test." + shortName;

        var c = db[shortName];
        s.adminCommand({shardcollection: longName, key: makeObjectDotted(1)});

        assert.eq(1, s.config.chunks.find({ns: longName}).count(), curT.name + " sanity check A");

        var unsorted = Array.shuffle(Object.extend([], curT.values));
        c.insert(makeObject(unsorted[0]));
        for (var x = 1; x < unsorted.length; x++) {
            c.save(makeObject(unsorted[x]));
        }

        assert.eq(6, c.find().count(), curT.name + " basic count");

        s.adminCommand({split: longName, middle: makeObjectDotted(curT.values[0])});
        s.adminCommand({split: longName, middle: makeObjectDotted(curT.values[2])});
        s.adminCommand({split: longName, middle: makeObjectDotted(curT.values[5])});

        s.adminCommand({
            movechunk: longName,
            find: makeObjectDotted(curT.values[2]),
            to: secondary.getMongo().name,
            _waitForDelete: true
        });

        s.printChunks();

        assert.eq(3, primary[shortName].find().toArray().length, curT.name + " primary count");
        assert.eq(3, secondary[shortName].find().toArray().length, curT.name + " secondary count");

        assert.eq(6, c.find().toArray().length, curT.name + " total count");
        assert.eq(6,
                  c.find().sort(makeObjectDotted(1)).toArray().length,
                  curT.name + " total count sorted");

        assert.eq(
            6, c.find().sort(makeObjectDotted(1)).count(), curT.name + " total count with count()");

        assert.eq(
            2,
            c.find({$or: [makeObjectDotted(curT.values[2]), makeObjectDotted(curT.values[4])]})
                .count(),
            curT.name + " $or count()");
        assert.eq(
            2,
            c.find({$or: [makeObjectDotted(curT.values[2]), makeObjectDotted(curT.values[4])]})
                .itcount(),
            curT.name + " $or itcount()");
        assert.eq(
            4,
            c.find({$nor: [makeObjectDotted(curT.values[2]), makeObjectDotted(curT.values[4])]})
                .count(),
            curT.name + " $nor count()");
        assert.eq(
            4,
            c.find({$nor: [makeObjectDotted(curT.values[2]), makeObjectDotted(curT.values[4])]})
                .itcount(),
            curT.name + " $nor itcount()");

        var stats = c.stats();
        printjson(stats);
        assert.eq(6, stats.count, curT.name + " total count with stats()");

        var count = 0;
        for (var shard in stats.shards) {
            count += stats.shards[shard].count;
        }
        assert.eq(6, count, curT.name + " total count with stats() sum");

        assert.eq(curT.values,
                  c.find().sort(makeObjectDotted(1)).toArray().map(getKey),
                  curT.name + " sort 1");
        assert.eq(curT.values,
                  c.find(makeInQuery()).sort(makeObjectDotted(1)).toArray().map(getKey),
                  curT.name + " sort 1 - $in");
        assert.eq(curT.values.reverse(),
                  c.find().sort(makeObjectDotted(-1)).toArray().map(getKey),
                  curT.name + " sort 2");

        assert.eq(0, c.find({xx: 17}).sort({zz: 1}).count(), curT.name + " xx 0a ");
        assert.eq(0, c.find({xx: 17}).sort(makeObjectDotted(1)).count(), curT.name + " xx 0b ");
        assert.eq(0, c.find({xx: 17}).count(), curT.name + " xx 0c ");
        assert.eq(0, c.find({xx: {$exists: true}}).count(), curT.name + " xx 1 ");

        c.update(makeObjectDotted(curT.values[3]), {$set: {xx: 17}});
        assert.eq(1, c.find({xx: {$exists: true}}).count(), curT.name + " xx 2 ");
        assert.eq(curT.values[3], getKey(c.findOne({xx: 17})), curT.name + " xx 3 ");

        assert.writeOK(
            c.update(makeObjectDotted(curT.values[3]), {$set: {xx: 17}}, {upsert: true}));

        assert.commandWorked(c.ensureIndex({_id: 1}, {unique: true}));

        // multi update
        var mysum = 0;
        c.find().forEach(function(z) {
            mysum += z.xx || 0;
        });
        assert.eq(17, mysum, curT.name + " multi update pre");

        c.update({}, {$inc: {xx: 1}}, false, true);

        var mysum = 0;
        c.find().forEach(function(z) {
            mysum += z.xx || 0;
        });
        assert.eq(23, mysum, curT.name + " multi update");
    }

    s.stop();

})();
