/**
 * Test basic read committed functionality, including:
 *  - Writes with writeConcern 'majority' should be visible once the write completes.
 *  - With the only data-bearing secondary down, committed reads should not include newly inserted
 *    data.
 *  - When data-bearing node comes back up and catches up, writes should be readable.
 */

(function() {
    "use strict";

    const majorityWriteConcern = {
        writeConcern: {w: "majority", wtimeout: 60 * 1000}
    };

    // Each test case includes a 'prepareCollection' method that sets up the initial state starting
    // with an empty collection, a 'write' method that does some write, and two arrays,
    // 'expectedBefore' and 'expectedAfter' that describe the expected contents of the collection
    // before and after the write. The 'prepareCollection' and 'write' methods should leave the
    // collection either empty or with a single document with _id: 1.
    const testCases = {
        insert: {
            prepareCollection: function(coll) {},  // No-op
            write: function(coll, writeConcern) {
                assert.writeOK(coll.insert({_id: 1}, writeConcern));
            },
            expectedBefore: [],
            expectedAfter: [{_id: 1}],
        },
        update: {
            prepareCollection: function(coll) {
                assert.writeOK(coll.insert({_id: 1, state: 'before'}, majorityWriteConcern));
            },
            write: function(coll, writeConcern) {
                assert.writeOK(coll.update({_id: 1}, {$set: {state: 'after'}}, writeConcern));
            },
            expectedBefore: [{_id: 1, state: 'before'}],
            expectedAfter: [{_id: 1, state: 'after'}],
        },
        remove: {
            prepareCollection: function(coll) {
                assert.writeOK(coll.insert({_id: 1}, majorityWriteConcern));
            },
            write: function(coll, writeConcern) {
                assert.writeOK(coll.remove({_id: 1}, writeConcern));
            },
            expectedBefore: [{_id: 1}],
            expectedAfter: [],
        },
    };

    // Set up a set and grab things for later.
    var name = "read_committed";
    var replTest =
        new ReplSetTest({name: name, nodes: 3, nodeOptions: {enableMajorityReadConcern: ''}});
    var nodes = replTest.nodeList();

    try {
        replTest.startSet();
    } catch (e) {
        var conn = MongoRunner.runMongod();
        if (!conn.getDB('admin').serverStatus().storageEngine.supportsCommittedReads) {
            jsTest.log("skipping test since storage engine doesn't support committed reads");
            MongoRunner.stopMongod(conn);
            return;
        }
        throw e;
    }

    var nodes = replTest.nodeList();
    var config = {
        "_id": name,
        "members": [
            {"_id": 0, "host": nodes[0]},
            {"_id": 1, "host": nodes[1], priority: 0},
            {"_id": 2, "host": nodes[2], arbiterOnly: true}
        ]
    };
    replTest.initiate(config);

    // Get connections and collection.
    var primary = replTest.getPrimary();
    var secondary = replTest.liveNodes.slaves[0];
    var coll = primary.getDB(name)[name];
    var secondaryColl = secondary.getDB(name)[name];

    function log(arg) {
        jsTest.log(tojson(arg));
    }

    function doRead(coll, readConcern) {
        readConcern.maxTimeMS = 3000;
        var res = assert.commandWorked(coll.runCommand('find', readConcern));
        return new DBCommandCursor(coll.getMongo(), res).toArray();
    }

    function doDirtyRead(coll) {
        log("doing dirty read");
        var ret = doRead(coll, {"readConcern": {"level": "local"}});
        log("done doing dirty read.");
        return ret;
    }

    function doCommittedRead(coll) {
        log("doing committed read");
        var ret = doRead(coll, {"readConcern": {"level": "majority"}});
        log("done doing committed read.");
        return ret;
    }

    function readLatestOplogEntry(readConcernLevel) {
        var oplog = primary.getDB('local').oplog.rs;
        var res = oplog.runCommand('find',
                                   {
                                     "readConcern": {"level": readConcernLevel},
                                     "maxTimeMS": 3000,
                                     sort: {$natural: -1},
                                     limit: 1,
                                   });
        assert.commandWorked(res);
        return new DBCommandCursor(coll.getMongo(), res).toArray()[0];
    }

    for (var testName in testCases) {
        jsTestLog('Running test ' + testName);
        var test = testCases[testName];

        const setUpInitialState = function setUpInitialState() {
            assert.writeOK(coll.remove({}, majorityWriteConcern));
            test.prepareCollection(coll);
            // Do some sanity checks.
            assert.eq(doDirtyRead(coll), test.expectedBefore);
            assert.eq(doCommittedRead(coll), test.expectedBefore);
        };

        // Writes done with majority write concern must be immediately visible to both dirty and
        // committed reads.
        setUpInitialState();
        test.write(coll, majorityWriteConcern);
        assert.eq(doDirtyRead(coll), test.expectedAfter);
        assert.eq(doCommittedRead(coll), test.expectedAfter);

        // Return to the initial state, then stop the secondary from applying new writes to prevent
        // them from becoming committed.
        setUpInitialState();
        assert.commandWorked(
            secondary.adminCommand({configureFailPoint: "rsSyncApplyStop", mode: "alwaysOn"}));
        const initialOplogTs = readLatestOplogEntry('local').ts;

        // Writes done without majority write concern must be immediately visible to dirty read
        // and hidden from committed reads until they have been replicated. The rules for seeing
        // an oplog entry for a write are the same as for the write itself.
        test.write(coll, {});
        assert.eq(doDirtyRead(coll), test.expectedAfter);
        assert.neq(readLatestOplogEntry('local').ts, initialOplogTs);
        assert.eq(doCommittedRead(coll), test.expectedBefore);
        assert.eq(readLatestOplogEntry('majority').ts, initialOplogTs);

        // Try the committed read again after sleeping to ensure it doesn't only work for
        // queries immediately after the write.
        sleep(1000);
        assert.eq(doCommittedRead(coll), test.expectedBefore);
        assert.eq(readLatestOplogEntry('majority').ts, initialOplogTs);

        // Restart oplog application on the secondary and ensure the committed view is updated.
        assert.commandWorked(
            secondary.adminCommand({configureFailPoint: "rsSyncApplyStop", mode: "off"}));
        coll.getDB().getLastError("majority", 60 * 1000);
        assert.eq(doCommittedRead(coll), test.expectedAfter);
        assert.neq(readLatestOplogEntry('majority').ts, initialOplogTs);

        // The secondary will be able to make the write committed soon after the primary, but there
        // is no way to block until it does.
        try {
            assert.soon(function() {
                return friendlyEqual(doCommittedRead(secondaryColl), test.expectedAfter);
            });
        } catch (e) {
            // generate useful error messages on failures.
            assert.eq(doCommittedRead(secondaryColl), test.expectedAfter);
        }
    }
}());
