/**
 * 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.
 *
 * @tags: [requires_majority_read_concern]
 */

import {ReplSetTest} from "jstests/libs/replsettest.js";
import {restartServerReplication, stopServerReplication} from "jstests/libs/write_concern_util.js";

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.commandWorked(coll.insert({_id: 1}, writeConcern));
        },
        expectedBefore: [],
        expectedAfter: [{_id: 1}],
    },
    update: {
        prepareCollection: function(coll) {
            assert.commandWorked(coll.insert({_id: 1, state: 'before'}, majorityWriteConcern));
        },
        write: function(coll, writeConcern) {
            assert.commandWorked(coll.update({_id: 1}, {$set: {state: 'after'}}, writeConcern));
        },
        expectedBefore: [{_id: 1, state: 'before'}],
        expectedAfter: [{_id: 1, state: 'after'}],
    },
    remove: {
        prepareCollection: function(coll) {
            assert.commandWorked(coll.insert({_id: 1}, majorityWriteConcern));
        },
        write: function(coll, writeConcern) {
            assert.commandWorked(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});

replTest.startSet();
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.getSecondary();
var coll = primary.getDB(name)[name];
var secondaryColl = secondary.getDB(name)[name];

// The default WC is majority and stopServerReplication will prevent satisfying any majority writes.
assert.commandWorked(primary.adminCommand(
    {setDefaultRWConcern: 1, defaultWriteConcern: {w: 1}, writeConcern: {w: "majority"}}));
replTest.awaitReplication();
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.getDB(), 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.getDB(), res).toArray()[0];
}

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

    const setUpInitialState = function setUpInitialState() {
        assert.commandWorked(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();
    stopServerReplication(secondary);
    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.
    restartServerReplication(secondary);
    replTest.awaitLastOpCommitted();

    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);
    }
}
replTest.stopSet();
