(function() {
'use strict';

load("jstests/sharding/libs/find_chunks_util.js");

// 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', s.shard1.shardName);

var db = s.getDB('test');
var primary = s.getPrimaryShard("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;
}

Random.setRandomSeed();

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,
              findChunksUtil.findChunksByNs(s.config, 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.commandWorked(
        c.update(makeObjectDotted(curT.values[3]), {$set: {xx: 17}}, {upsert: true}));

    assert.commandWorked(c.createIndex({_id: 1}));

    // 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();
})();
