// Test parsing of readConcern level 'snapshot' and the presence of the 'atClusterTime' field in
// snapshot cursor responses.
// @tags: [
//   requires_majority_read_concern,
//   requires_persistence,
//   requires_replication,
//   uses_transactions,
// ]
import {ReplSetTest} from "jstests/libs/replsettest.js";

const dbName = "test";
const collName = "coll";

//
// Configurations.
//

// Transactions should fail on storage engines that do not support them.
let rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();
let session = rst.getPrimary().getDB(dbName).getMongo().startSession({causalConsistency: false});
let sessionDb = session.getDatabase(dbName);
if (!sessionDb.serverStatus().storageEngine.supportsSnapshotReadConcern) {
    // Transactions with readConcern snapshot fail.
    session.startTransaction({readConcern: {level: "snapshot"}});
    assert.commandFailedWithCode(sessionDb.runCommand({find: collName}),
                                 ErrorCodes.IllegalOperation);
    assert.commandFailedWithCode(session.abortTransaction_forTesting(),
                                 [ErrorCodes.NoSuchTransaction, ErrorCodes.IllegalOperation]);

    // Transactions without readConcern snapshot fail.
    session.startTransaction();
    assert.commandFailedWithCode(sessionDb.runCommand({find: collName}),
                                 ErrorCodes.IllegalOperation);
    assert.commandFailedWithCode(session.abortTransaction_forTesting(),
                                 [ErrorCodes.NoSuchTransaction, ErrorCodes.IllegalOperation]);

    rst.stopSet();
    quit();
}
session.endSession();
rst.stopSet();

// readConcern 'snapshot' is not allowed on a standalone.
const conn = MongoRunner.runMongod();
session = conn.startSession({causalConsistency: false});
sessionDb = session.getDatabase(dbName);
assert.neq(null, conn, "mongod was unable to start up");
session.startTransaction({readConcern: {level: "snapshot"}});
assert.commandFailedWithCode(sessionDb.runCommand({find: collName}), ErrorCodes.IllegalOperation);
assert.commandFailedWithCode(session.abortTransaction_forTesting(), ErrorCodes.IllegalOperation);
session.endSession();
MongoRunner.stopMongod(conn);

// readConcern 'snapshot' is allowed on a replica set primary.
rst = new ReplSetTest({nodes: 2});
rst.startSet();
rst.initiate();
assert.commandWorked(
    rst.getPrimary().getDB(dbName).runCommand({create: collName, writeConcern: {w: "majority"}}));
session = rst.getPrimary().getDB(dbName).getMongo().startSession({causalConsistency: false});
sessionDb = session.getDatabase(dbName);
session.startTransaction({writeConcern: {w: "majority"}, readConcern: {level: "snapshot"}});
assert.commandWorked(sessionDb.coll.insert({}));
assert.commandWorked(sessionDb.runCommand({find: collName}));
assert.commandWorked(session.commitTransaction_forTesting());

// readConcern 'snapshot' is allowed with 'afterClusterTime'.
session.startTransaction();
let pingRes = assert.commandWorked(rst.getPrimary().adminCommand({ping: 1}));
assert(pingRes.hasOwnProperty("$clusterTime"), tojson(pingRes));
assert(pingRes.$clusterTime.hasOwnProperty("clusterTime"), tojson(pingRes));
assert.commandWorked(sessionDb.runCommand({
    find: collName,
    readConcern: {level: "snapshot", afterClusterTime: pingRes.$clusterTime.clusterTime}
}));
assert.commandWorked(session.commitTransaction_forTesting());

// readConcern 'snapshot' is not allowed with 'afterOpTime'.
session.startTransaction(
    {readConcern: {level: "snapshot", afterOpTime: {ts: Timestamp(1, 2), t: 1}}});
assert.commandFailedWithCode(sessionDb.runCommand({find: collName}), ErrorCodes.InvalidOptions);
assert.commandFailedWithCode(session.abortTransaction_forTesting(), ErrorCodes.NoSuchTransaction);
session.endSession();

pingRes = assert.commandWorked(rst.getSecondary().adminCommand({ping: 1}));
assert(pingRes.hasOwnProperty("$clusterTime"), tojson(pingRes));
assert(pingRes.$clusterTime.hasOwnProperty("clusterTime"), tojson(pingRes));

session.startTransaction(
    {readConcern: {level: "snapshot", afterClusterTime: pingRes.$clusterTime.clusterTime}});
assert.commandWorked(sessionDb.runCommand({find: collName}));
assert.commandWorked(session.commitTransaction_forTesting());

session.endSession();
rst.stopSet();

//
// Commands.
//

rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();
let testDB = rst.getPrimary().getDB(dbName);
assert.commandWorked(testDB.runCommand({create: collName, writeConcern: {w: "majority"}}));

// Test snapshot in a transaction.
session = testDB.getMongo().startSession({causalConsistency: false});
sessionDb = session.getDatabase(dbName);

// readConcern 'snapshot' is supported by find in a transaction.
session.startTransaction({readConcern: {level: "snapshot"}, writeConcern: {w: "majority"}});
let res = assert.commandWorked(sessionDb.runCommand({find: collName, batchSize: 0}));
assert(!res.cursor.hasOwnProperty("atClusterTime"), tojson(res));

// readConcern 'snapshot' is supported by getMore in a transaction.
res = assert.commandWorked(sessionDb.runCommand({getMore: res.cursor.id, collection: collName}));
assert(!res.cursor.hasOwnProperty("atClusterTime"), tojson(res));

// readConcern 'snapshot' is supported by aggregate in a transaction.
res = assert.commandWorked(sessionDb.runCommand({aggregate: collName, pipeline: [], cursor: {}}));
assert(!res.cursor.hasOwnProperty("atClusterTime"), tojson(res));

// readConcern 'snapshot' is supported by distinct in a transaction.
res = assert.commandWorked(sessionDb.runCommand({distinct: collName, key: "x"}));
assert(!res.hasOwnProperty("atClusterTime"), tojson(res));

// readConcern 'snapshot' is not supported by non-CRUD commands in a transaction.
assert.commandFailedWithCode(sessionDb.runCommand({dropIndexes: collName, index: "a_1"}),
                             ErrorCodes.OperationNotSupportedInTransaction);
assert.commandWorked(session.abortTransaction_forTesting());
session.endSession();

// Test snapshot outside of transactions.
const snapshotReadConcern = {
    level: "snapshot"
};
// readConcern 'snapshot' is supported by find outside of transactions.
res = assert.commandWorked(
    testDB.runCommand({find: collName, batchSize: 0, readConcern: snapshotReadConcern}));
assert(res.cursor.hasOwnProperty("atClusterTime"), tojson(res));

// readConcern 'snapshot' is supported by getMore outside of a transaction.
res = assert.commandWorked(testDB.runCommand({getMore: res.cursor.id, collection: collName}));
assert(res.cursor.hasOwnProperty("atClusterTime"), tojson(res));

// readConcern 'snapshot' is supported by aggregate outside of transactions.
res = assert.commandWorked(testDB.runCommand(
    {aggregate: collName, pipeline: [], cursor: {}, readConcern: snapshotReadConcern}));
assert(res.cursor.hasOwnProperty("atClusterTime"), tojson(res));

// readConcern 'snapshot' is supported by distinct outside of transactions.
res = assert.commandWorked(
    testDB.runCommand({distinct: collName, key: "x", readConcern: snapshotReadConcern}));
assert(res.hasOwnProperty("atClusterTime"), tojson(res));

// readConcern 'snapshot' is not supported by count.
assert.commandFailedWithCode(testDB.runCommand({count: collName, readConcern: snapshotReadConcern}),
                             ErrorCodes.InvalidOptions);

// readConcern 'snapshot' is not supported by findAndModify.
assert.commandFailedWithCode(testDB.runCommand({
    findAndModify: collName,
    query: {},
    update: {$set: {a: 1}},
    readConcern: snapshotReadConcern,
}),
                             ErrorCodes.InvalidOptions);

// readConcern 'snapshot' is not supported by non-CRUD commands.
assert.commandFailedWithCode(
    testDB.adminCommand({listDatabases: 1, readConcern: snapshotReadConcern}),
    ErrorCodes.InvalidOptions);

assert.commandFailedWithCode(
    testDB.runCommand({listCollections: 1, readConcern: snapshotReadConcern}),
    ErrorCodes.InvalidOptions);

assert.commandFailedWithCode(
    testDB.runCommand({listIndexes: collName, readConcern: snapshotReadConcern}),
    ErrorCodes.InvalidOptions);

assert.commandFailedWithCode(
    testDB.runCommand({dropIndexes: collName, index: "a_1", readConcern: snapshotReadConcern}),
    ErrorCodes.InvalidOptions);

rst.stopSet();