/**
 * Tests for the command assertion functions in mongo/shell/assert.js.
 *  @tags: [
 *   requires_scripting,
 * ]
 */

const conn = MongoRunner.runMongod();
const db = conn.getDB("commandAssertions");
const kFakeErrCode = 1234567890;
const tests = [];

const sampleWriteConcernError = {
    n: 1,
    ok: 1,
    writeConcernError: {
        code: ErrorCodes.WriteConcernFailed,
        codeName: "WriteConcernFailed",
        errmsg: "waiting for replication timed out",
        errInfo: {
            wtimeout: true,
        },
    },
};

function setup() {
    db.coll.drop();
    assert.commandWorked(db.coll.insert({_id: 1}));
}

// Raw command responses.
tests.push(function rawCommandOk() {
    const res = db.runCommand({"ping": 1});
    assert.doesNotThrow(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.throws(() => assert.commandFailed(res));
    assert.throws(() => assert.commandFailedWithCode(res, 0));
});

function _assertMsgFunctionExecution(
    assertFunc, assertParameter, {expectException: expectException = false} = {}) {
    var msgFunctionCalled = false;
    var expectedAssert = assert.doesNotThrow;

    if (expectException) {
        expectedAssert = assert.throws;
    }

    expectedAssert(() => {
        assertFunc(assertParameter, () => {
            msgFunctionCalled = true;
        });
    });

    assert.eq(expectException, msgFunctionCalled, "msg function execution should match assertion");
}

tests.push(function msgFunctionOnlyCalledOnFailure() {
    const res = db.runCommand({"ping": 1});

    _assertMsgFunctionExecution(assert.commandWorked, res, {expectException: false});
    _assertMsgFunctionExecution(
        assert.commandWorkedIgnoringWriteErrors, res, {expectException: false});
    _assertMsgFunctionExecution(assert.commandFailed, res, {expectException: true});

    var msgFunctionCalled = false;
    assert.throws(() => assert.commandFailedWithCode(res, 0, () => {
        msgFunctionCalled = true;
    }));
    assert.eq(true, msgFunctionCalled, "msg function execution should match assertion");
});

tests.push(function rawCommandErr() {
    const res = db.runCommand({"IHopeNobodyEverMakesThisACommand": 1});
    assert.throws(() => assert.commandWorked(res));
    assert.throws(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
    assert.doesNotThrow(() => assert.commandFailedWithCode(res, ErrorCodes.CommandNotFound));
    // commandFailedWithCode should succeed if any of the passed error codes are matched.
    assert.doesNotThrow(
        () => assert.commandFailedWithCode(res, [ErrorCodes.CommandNotFound, kFakeErrCode]));
    assert.doesNotThrow(() => assert.commandWorkedOrFailedWithCode(
                            res,
                            [ErrorCodes.CommandNotFound, kFakeErrCode],
                            "threw even though failed with correct error codes"));
    assert.throws(
        () => assert.commandWorkedOrFailedWithCode(
            res, [kFakeErrCode], "didn't throw even though failed with incorrect error code"));
});

tests.push(function rawCommandWriteOk() {
    const res = db.runCommand({insert: "coll", documents: [{_id: 2}]});
    assert.doesNotThrow(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.throws(() => assert.commandFailed(res));
    assert.throws(() => assert.commandFailedWithCode(res, 0));
    assert.doesNotThrow(() => assert.commandWorkedOrFailedWithCode(res, 0));
});

tests.push(function rawCommandWriteErr() {
    const res = db.runCommand({insert: "coll", documents: [{_id: 1}]});
    assert.throws(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
    assert.doesNotThrow(() => assert.commandFailedWithCode(res, ErrorCodes.DuplicateKey));
    assert.doesNotThrow(
        () => assert.commandFailedWithCode(res, [ErrorCodes.DuplicateKey, kFakeErrCode]));
    assert.doesNotThrow(
        () => assert.commandWorkedOrFailedWithCode(
            res, [ErrorCodes.DuplicateKey, kFakeErrCode], "expected a write failure"));
    assert.throws(() => assert.commandWorkedOrFailedWithCode(
                      res, [kFakeErrCode], "expected to throw on write error"));
});

tests.push(function collInsertWriteOk() {
    const res = db.coll.insert({_id: 2});
    assert(res instanceof WriteResult);
    assert.doesNotThrow(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.throws(() => assert.commandFailed(res));
    assert.throws(() => assert.commandFailedWithCode(res, 0));
});

tests.push(function collInsertWriteErr() {
    const res = db.coll.insert({_id: 1});
    assert(res instanceof WriteResult);
    assert.throws(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
    assert.doesNotThrow(() => assert.commandFailedWithCode(res, ErrorCodes.DuplicateKey));
    assert.doesNotThrow(
        () => assert.commandFailedWithCode(res, [ErrorCodes.DuplicateKey, kFakeErrCode]));
});

tests.push(function collMultiInsertWriteOk() {
    const res = db.coll.insert([{_id: 3}, {_id: 2}]);
    assert(res instanceof BulkWriteResult);
    assert.doesNotThrow(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.throws(() => assert.commandFailed(res));
    assert.throws(() => assert.commandFailedWithCode(res, 0));
    assert.doesNotThrow(
        () => assert.commandWorkedOrFailedWithCode(res, 0, "threw even though succeeded"));
});

tests.push(function collMultiInsertWriteErr() {
    const res = db.coll.insert([{_id: 1}, {_id: 2}]);
    assert(res instanceof BulkWriteResult);
    assert.throws(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
    assert.doesNotThrow(() => assert.commandFailedWithCode(res, ErrorCodes.DuplicateKey));
    assert.doesNotThrow(
        () => assert.commandFailedWithCode(res, [ErrorCodes.DuplicateKey, kFakeErrCode]));
});

// Test when the insert command fails with ok:0 (i.e. not failing due to write err)
tests.push(function collInsertCmdErr() {
    const res = db.coll.insert({x: 1}, {writeConcern: {"bad": 1}});
    assert(res instanceof WriteCommandError);
    assert.throws(() => assert.commandWorked(res));
    assert.throws(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
});

tests.push(function collMultiInsertCmdErr() {
    const res = db.coll.insert([{x: 1}, {x: 2}], {writeConcern: {"bad": 1}});
    assert(res instanceof WriteCommandError);
    assert.throws(() => assert.commandWorked(res));
    assert.throws(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
});

tests.push(function mapReduceOk() {
    const res = db.coll.mapReduce(
        function() {
            emit(this._id, 0);
        },
        function(k, v) {
            return v[0];
        },
        {out: "coll_out"});
    assert.doesNotThrow(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.throws(() => assert.commandFailed(res));
    assert.throws(() => assert.commandFailedWithCode(res, 0));
});

tests.push(function crudInsertOneOk() {
    const res = db.coll.insertOne({_id: 2});
    assert(res.hasOwnProperty("acknowledged"));
    assert.doesNotThrow(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.throws(() => assert.commandFailed(res));
    assert.throws(() => assert.commandFailedWithCode(res, 0));
});

tests.push(function crudInsertOneErr() {
    let threw = false;
    let res = null;
    try {
        db.coll.insertOne({_id: 1});
    } catch (e) {
        threw = true;
        res = e;
    }
    assert(threw);
    assert(res instanceof WriteError);
    assert.throws(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
    assert.doesNotThrow(() => assert.commandFailedWithCode(res, ErrorCodes.DuplicateKey));
    assert.doesNotThrow(
        () => assert.commandFailedWithCode(res, [ErrorCodes.DuplicateKey, kFakeErrCode]));
});

tests.push(function crudInsertManyOk() {
    const res = db.coll.insertMany([{_id: 2}, {_id: 3}]);
    assert(res.hasOwnProperty("acknowledged"));
    assert.doesNotThrow(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.throws(() => assert.commandFailed(res));
    assert.throws(() => assert.commandFailedWithCode(res, 0));
});

tests.push(function crudInsertManyErr() {
    let threw = false;
    let res = null;
    try {
        db.coll.insertMany([{_id: 1}, {_id: 2}]);
    } catch (e) {
        threw = true;
        res = e;
    }
    assert(threw);
    assert(res instanceof BulkWriteError);
    assert.throws(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
    assert.doesNotThrow(() => assert.commandFailedWithCode(res, ErrorCodes.DuplicateKey));
    assert.doesNotThrow(
        () => assert.commandFailedWithCode(res, [ErrorCodes.DuplicateKey, kFakeErrCode]));
});

tests.push(function rawMultiWriteErr() {
    // Do an unordered bulk insert with duplicate keys to produce multiple write errors.
    const res = db.runCommand({"insert": "coll", documents: [{_id: 1}, {_id: 1}], ordered: false});
    assert(res.writeErrors.length == 2, "did not get multiple write errors");
    assert.throws(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
    assert.doesNotThrow(() => assert.commandFailedWithCode(res, ErrorCodes.DuplicateKey));
    assert.doesNotThrow(
        () => assert.commandFailedWithCode(res, [ErrorCodes.DuplicateKey, kFakeErrCode]));
});

tests.push(function bulkMultiWriteErr() {
    // Do an unordered bulk insert with duplicate keys to produce multiple write errors.
    const res = db.coll.insert([{_id: 1}, {_id: 1}], {ordered: false});
    assert.throws(() => assert.commandWorked(res));
    assert.doesNotThrow(() => assert.commandWorkedIgnoringWriteErrors(res));
    assert.doesNotThrow(() => assert.commandFailed(res));
    assert.doesNotThrow(() => assert.commandFailedWithCode(res, ErrorCodes.DuplicateKey));
    assert.doesNotThrow(
        () => assert.commandFailedWithCode(res, [ErrorCodes.DuplicateKey, kFakeErrCode]));
});

tests.push(function writeConcernErrorCausesCommandWorkedToAssert() {
    const result = sampleWriteConcernError;

    assert.throws(() => {
        assert.commandWorked(result);
    });
});

tests.push(function writeConcernErrorCausesCommandFailedToPass() {
    const result = sampleWriteConcernError;

    assert.doesNotThrow(() => {
        assert.commandFailed(result);
        assert.commandFailedWithCode(result, ErrorCodes.WriteConcernFailed);
    });
});

tests.push(function writeConcernErrorCanBeIgnored() {
    const result = sampleWriteConcernError;

    assert.doesNotThrow(() => {
        assert.commandWorkedIgnoringWriteConcernErrors(result);
    });
});

tests.push(function invalidResponsesAttemptToProvideInformationToCommandWorks() {
    const invalidResponses = [undefined, 'not a valid response', 42];

    invalidResponses.forEach((invalidRes) => {
        const error = assert.throws(() => {
            assert.commandWorked(invalidRes);
        });

        assert.gte(error.message.indexOf(invalidRes), 0);
        assert.gte(error.message.indexOf(typeof invalidRes), 0);
    });
});

tests.push(function invalidResponsesAttemptToProvideInformationCommandFailed() {
    const invalidResponses = [undefined, 'not a valid response', 42];

    invalidResponses.forEach((invalidRes) => {
        const error = assert.throws(() => {
            assert.commandFailed(invalidRes);
        });

        assert.gte(error.message.indexOf(invalidRes), 0);
        assert.gte(error.message.indexOf(typeof invalidRes), 0);
    });
});

tests.push(function assertCallsHangAnalyzer() {
    function runAssertTest(f, expectCall) {
        const oldMongoRunner = MongoRunner;
        let runs = 0;
        try {
            MongoRunner.runHangAnalyzer = function() {
                ++runs;
            };
            f();
            assert(false);
        } catch (e) {
            if (expectCall) {
                assert.eq(runs, 1);
            } else {
                assert.eq(runs, 0);
            }
        } finally {
            MongoRunner = oldMongoRunner;
        }
    }
    const nonTimeOutWriteConcernError = {
        n: 1,
        ok: 1,
        writeConcernError: {
            code: ErrorCodes.WriteConcernFailed,
            codeName: "WriteConcernFailed",
            errmsg: "foo",
        },
    };

    const lockTimeoutError = {
        ok: 0,
        errmsg: "Unable to acquire lock",
        code: ErrorCodes.LockTimeout,
        codeName: "LockTimeout",
    };

    const lockTimeoutTransientTransactionError = {
        errorLabels: ["TransientTransactionError"],
        ok: 0,
        errmsg: "Unable to acquire lock",
        code: ErrorCodes.LockTimeout,
        codeName: "LockTimeout",
    };

    runAssertTest(() => assert.commandWorked(sampleWriteConcernError), true);
    runAssertTest(() => assert.commandWorked(nonTimeOutWriteConcernError), false);

    runAssertTest(() => assert.commandFailed(sampleWriteConcernError), false);

    runAssertTest(
        () => assert.commandFailedWithCode(sampleWriteConcernError, ErrorCodes.DuplicateKey), true);
    runAssertTest(
        () => assert.commandFailedWithCode(nonTimeOutWriteConcernError, ErrorCodes.DuplicateKey),
        false);
    runAssertTest(
        () => assert.commandFailedWithCode(sampleWriteConcernError, ErrorCodes.WriteConcernFailed),
        false);

    runAssertTest(() => assert.commandWorkedIgnoringWriteConcernErrors(sampleWriteConcernError),
                  false);

    runAssertTest(() => assert.commandWorked(lockTimeoutError), true);
    runAssertTest(() => assert.commandFailed(lockTimeoutError), false);
    runAssertTest(() => assert.commandFailedWithCode(lockTimeoutError, ErrorCodes.DuplicateKey),
                  true);
    runAssertTest(() => assert.commandFailedWithCode(lockTimeoutError, ErrorCodes.LockTimeout),
                  false);

    runAssertTest(() => assert.commandWorked(lockTimeoutTransientTransactionError), false);
    runAssertTest(() => assert.commandFailed(lockTimeoutTransientTransactionError), false);
    runAssertTest(() => assert.commandFailedWithCode(lockTimeoutTransientTransactionError,
                                                     ErrorCodes.DuplicateKey),
                  false);
    runAssertTest(() => assert.commandFailedWithCode(lockTimeoutTransientTransactionError,
                                                     ErrorCodes.LockTimeout),
                  false);
});

tests.forEach((test) => {
    jsTest.log(`Starting test '${test.name}'`);
    setup();
    const oldMongoRunner = MongoRunner;
    try {
        // We shouldn't actually run the hang-analyzer for these tests.
        MongoRunner.runHangAnalyzer = Function.prototype;
        test();
    } finally {
        MongoRunner = oldMongoRunner;
    }
});

/* cleanup */
MongoRunner.stopMongod(conn);
