/**
 * Tests index usage with durable history across restarts.
 *
 * @tags: [
 *     requires_persistence,
 *     requires_replication,
 * ]
 */
import {configureFailPoint} from "jstests/libs/fail_point_util.js";
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {IndexBuildTest} from "jstests/noPassthrough/libs/index_build.js";

const replTest = new ReplSetTest({
    nodes: 1,
    nodeOptions: {
        setParameter: {
            // To control durable history more predictably, disable the checkpoint thread.
            syncdelay: 0
        }
    }
});
replTest.startSet();
replTest.initiate();

const indexSpec = {
    a: 1
};

const primary = function() {
    return replTest.getPrimary();
};

const testDB = function() {
    return primary().getDB("test");
};

const coll = function() {
    return testDB()[jsTestName()];
};

const insert = function(document) {
    // The write concern guarantees the stable and oldest timestamp are bumped.
    return assert
        .commandWorked(testDB().runCommand(
            {insert: coll().getName(), documents: [document], writeConcern: {w: "majority"}}))
        .operationTime;
};

const findWithIndex = function(atClusterTime, expectedErrCode) {
    let res = {};
    if (atClusterTime == undefined) {
        res = testDB().runCommand({find: jsTestName(), hint: indexSpec});
    } else {
        res = testDB().runCommand({
            find: jsTestName(),
            hint: indexSpec,
            readConcern: {level: "snapshot", atClusterTime: atClusterTime}
        });
    }

    if (expectedErrCode) {
        assert.commandFailedWithCode(res, expectedErrCode);
    } else {
        return assert.commandWorked(res);
    }
};

const oldestTS = insert({a: 0});
jsTestLog("Oldest timestamp: " + tojson(oldestTS));

// The index does not exist yet.
findWithIndex(undefined, ErrorCodes.BadValue);
findWithIndex(oldestTS, ErrorCodes.BadValue);

const createIndexTS = assert.commandWorked(coll().createIndex(indexSpec)).operationTime;
jsTestLog("Initial index creation timestamp: " + tojson(createIndexTS));

// The index is only available for use as of the 'createIndexTS'.
findWithIndex(oldestTS, ErrorCodes.BadValue);
assert.eq(1, findWithIndex(createIndexTS)["cursor"]["firstBatch"].length);
assert.eq(1, findWithIndex(undefined)["cursor"]["firstBatch"].length);

// Rebuild the index without finishing it.
assert.commandWorked(coll().dropIndex(indexSpec));
const fp = configureFailPoint(primary(), "hangAfterSettingUpIndexBuildUnlocked");
const awaitCreateIndex = IndexBuildTest.startIndexBuild(primary(), coll().getFullName(), indexSpec);
fp.wait();

// Get a timestamp after starting the index build but before the commit timestamp of the index
// build.
const preIndexCommitTS = insert({a: 1});

assert.commandWorked(testDB().adminCommand({fsync: 1}));

replTest.stop(0, 9, {allowedExitCode: MongoRunner.EXIT_SIGKILL}, {forRestart: true});
awaitCreateIndex({checkExitSuccess: false});

replTest.start(
    0,
    {
        setParameter: {
            "failpoint.hangAfterSettingUpIndexBuildUnlocked": tojson({mode: "alwaysOn"}),
            // To control durable history more predictably, disable the checkpoint thread.
            syncdelay: 0
        }
    },
    true /* restart */);

const checkLogs = function() {
    // Found index from unfinished build.
    checkLog.containsJson(primary(), 22253, {
        index: "a_1",
        namespace: coll().getFullName(),
    });

    // Resetting unfinished index.
    checkLog.containsJson(primary(), 6987700, {namespace: coll().getFullName(), index: "a_1"});

    // Index build restarting.
    checkLog.containsJson(primary(), 20660);
};

checkLogs();

// The index is being re-created.

// It's possible to read prior to the most recent DDL operation for the collection.
//
// At oldestTs, the index did not exist, so queries for the index at that timestamp will return
// BadValue.
//
// At createIndexTS, the index did exist, so queries for the index at that timestamp will be
// successful.
//
// At preIndexCommitTS, the new index has not finished being created yet, so queries for the index
// at that timestamp will return BadValue.
//
// Etc.
//
// Find queries should all return the result one would expect based on the state of the catalog at
// that point in time. When the feature flag is disabled, these find queries will instead return
// SnapshotUnavailable.

findWithIndex(oldestTS, ErrorCodes.BadValue);
findWithIndex(createIndexTS, null);
findWithIndex(preIndexCommitTS, ErrorCodes.BadValue);
findWithIndex(undefined, ErrorCodes.BadValue);

const restartInsertTS = insert({a: 2});

// Take a checkpoint to persist the new catalog entry of the index being rebuilt.
assert.commandWorked(testDB().adminCommand({fsync: 1}));

replTest.stop(0, 9, {allowedExitCode: MongoRunner.EXIT_SIGKILL}, {forRestart: true});
replTest.start(
    0,
    {
        setParameter: {
            // To control durable history more predictably, disable the checkpoint thread.
            syncdelay: 0
        }
    },
    true /* restart */);

checkLogs();

// Startup recovery will rebuild the index in the background. Wait for the index build to finish.
checkLog.containsJson(primary(), 20663, {
    namespace: coll().getFullName(),
    indexesBuilt: ["a_1"],
    numIndexesAfter: 2,
});
IndexBuildTest.assertIndexes(coll(), 2, ["_id_", "a_1"]);

findWithIndex(oldestTS, ErrorCodes.BadValue);
findWithIndex(createIndexTS, null);
findWithIndex(preIndexCommitTS, ErrorCodes.BadValue);
findWithIndex(restartInsertTS, ErrorCodes.BadValue);

assert.eq(3, findWithIndex(undefined)["cursor"]["firstBatch"].length);

const insertAfterIndexBuildTS = insert({a: 3});
assert.eq(4, findWithIndex(insertAfterIndexBuildTS)["cursor"]["firstBatch"].length);
assert.eq(4, findWithIndex(undefined)["cursor"]["firstBatch"].length);

// Demonstrate that durable history can be used across a restart with a finished index.
assert.commandWorked(testDB().adminCommand({fsync: 1}));
replTest.restart(primary());

assert.eq(4, findWithIndex(undefined)["cursor"]["firstBatch"].length);
const insertAfterRestartAfterIndexBuild = insert({a: 4});
assert.eq(5, findWithIndex(insertAfterRestartAfterIndexBuild)["cursor"]["firstBatch"].length);
assert.eq(5, findWithIndex(undefined)["cursor"]["firstBatch"].length);

findWithIndex(oldestTS, ErrorCodes.BadValue);
findWithIndex(createIndexTS, null);
findWithIndex(preIndexCommitTS, ErrorCodes.BadValue);
findWithIndex(restartInsertTS, ErrorCodes.BadValue);

assert.eq(4, findWithIndex(insertAfterIndexBuildTS)["cursor"]["firstBatch"].length);

// Drop the index and demonstrate the durable history can be used across a restart for reads with
// times prior to the drop.
const dropIndexTS = assert.commandWorked(coll().dropIndex(indexSpec)).operationTime;
jsTestLog("Index drop timestamp: " + tojson(dropIndexTS));

// Take a checkpoint to persist the new catalog entry of the index being rebuilt.
assert.commandWorked(testDB().adminCommand({fsync: 1}));

replTest.stop(0, 9, {allowedExitCode: MongoRunner.EXIT_SIGKILL}, {forRestart: true});
replTest.start(
    0,
    {
        setParameter: {
            // To control durable history more predictably, disable the checkpoint thread.
            syncdelay: 0
        }
    },
    true /* restart */);

// Test that we can read using the dropped index on timestamps before the drop
assert.eq(4, findWithIndex(insertAfterIndexBuildTS)["cursor"]["firstBatch"].length);
assert.eq(5, findWithIndex(insertAfterRestartAfterIndexBuild)["cursor"]["firstBatch"].length);

replTest.stopSet();
