/**
 * Tests for the assertion functions in mongo/shell/assert.js.
 */

const tests = [];

const kDefaultTimeoutMS = 10 * 1000;
const kSmallTimeoutMS = 200;
const kSmallRetryIntervalMS = 1;
const kDefaultRetryAttempts = 5;

/* doassert tests */

tests.push(function callingDoAssertWithStringThrowsException() {
    const expectedError = 'hello world';
    const actualError = assert.throws(() => {
        doassert(expectedError);
    });

    assert.eq(
        'Error: ' + expectedError, actualError, 'doAssert should throw passed msg as exception');
});

tests.push(function callingDoAssertWithObjectThrowsException() {
    const expectedError = {err: 'hello world'};
    const actualError = assert.throws(() => {
        doassert(expectedError);
    });

    assert.eq('Error: ' + tojson(expectedError),
              actualError,
              'doAssert should throw passed object as exception');
});

tests.push(function callingDoAssertWithStringPassedAsFunctionThrowsException() {
    const expectedError = 'hello world';
    const actualError = assert.throws(() => {
        doassert(() => {
            return expectedError;
        });
    });

    assert.eq(
        'Error: ' + expectedError, actualError, 'doAssert should throw passed msg as exception');
});

tests.push(function callingDoAssertWithObjectAsFunctionThrowsException() {
    const expectedError = {err: 'hello world'};
    const actualError = assert.throws(() => {
        doassert(() => {
            return expectedError;
        });
    });

    assert.eq('Error: ' + tojson(expectedError),
              actualError,
              'doAssert should throw passed object as exception');
});

tests.push(function callingDoAssertCorrectlyAttachesWriteErrors() {
    const errorMessage = "Operation was interrupted";
    const bulkResult = {
        nModified: 0,
        n: 0,
        writeErrors: [{"index": 0, "code": 11601, "errmsg": errorMessage}],
        upserted: [],
        ok: 1
    };

    let error = assert.throws(() => doassert(errorMessage, new BulkWriteError(bulkResult)));
    assert.eq(error.writeErrors[0].code, bulkResult.writeErrors[0].code);

    error = assert.throws(() => doassert(errorMessage, new BulkWriteResult(bulkResult)));
    assert.eq(error.writeErrors[0].code, bulkResult.writeErrors[0].code);

    error = assert.throws(() => doassert(errorMessage, new WriteResult(bulkResult)));
    assert.eq(error.writeErrors[0].code, bulkResult.writeErrors[0].code);

    error = assert.throws(() => doassert(errorMessage, bulkResult));
    assert.eq(error.writeErrors[0].code, bulkResult.writeErrors[0].code);
});

tests.push(function callingDoAssertCorrectlyAttachesWriteConcernError() {
    const errorMessage = "Operation was interrupted";
    const bulkResult = {
        nModified: 0,
        n: 0,
        writeConcernErrors:
            [{code: 6, codeName: "HostUnreachable", errmsg: errorMessage, errInfo: {}}],
        upserted: [],
        ok: 1
    };

    let error = assert.throws(() => doassert(errorMessage, new BulkWriteError(bulkResult)));
    assert.eq(error.writeConcernError.code, bulkResult.writeConcernErrors[0].code);

    error = assert.throws(() => doassert(errorMessage, new BulkWriteResult(bulkResult)));
    assert.eq(error.writeConcernError.code, bulkResult.writeConcernErrors[0].code);

    error = assert.throws(() => doassert(errorMessage, new WriteResult(bulkResult)));
    assert.eq(error.writeConcernError.code, bulkResult.writeConcernErrors[0].code);

    error = assert.throws(() => doassert(errorMessage, bulkResult));
    assert.eq(error.writeConcernError.code, bulkResult.writeConcernErrors[0].code);
});

/* assert tests */

tests.push(function assertShouldFailForMoreThan2Args() {
    const err = assert.throws(() => {
        assert(1, 2, 3);
    });
    assert.neq(-1,
               err.message.indexOf('Too many parameters'),
               'Too many params message should be displayed');
});

tests.push(function assertShouldNotThrowExceptionForTrue() {
    assert.doesNotThrow(() => {
        assert(true, 'message');
    });
});

tests.push(function assertShouldThrowExceptionForFalse() {
    const expectedMessage = 'message';
    const err = assert.throws(() => {
        assert(false, expectedMessage);
    });

    assert.neq(
        -1, err.message.indexOf(expectedMessage), 'assert message should be thrown on error');
});

tests.push(function assertShouldThrowExceptionForFalseWithDefaultMessage() {
    const defaultMessage = 'assert failed';
    const err = assert.throws(() => {
        assert(false);
    });

    assert.eq(defaultMessage, err.message, 'assert message should be thrown on error');
});

tests.push(function assertShouldThrowExceptionForFalseWithDefaultMessagePrefix() {
    const prefix = 'assert failed';
    const message = 'the assertion failed';
    const err = assert.throws(() => {
        assert(false, message);
    });

    assert.neq(-1, err.message.indexOf(prefix), 'assert message should should contain prefix');
    assert.neq(
        -1, err.message.indexOf(message), 'assert message should should contain original message');
});

tests.push(function assertShouldNotCallMsgFunctionsOnSuccess() {
    var called = false;

    assert(true, () => {
        called = true;
    });

    assert.eq(false, called, 'called should not have been udpated');
});

tests.push(function assertShouldCallMsgFunctionsOnFailure() {
    var called = false;

    assert.throws(() => {
        assert(false, () => {
            called = true;
            return 'error message';
        });
    });

    assert.eq(true, called, 'called should not have been udpated');
});

tests.push(function assertShouldAcceptObjectAsMsg() {
    const objMsg = {someMessage: 1};
    const err = assert.throws(() => {
        assert(false, objMsg);
    });

    assert.neq(-1,
               err.message.indexOf(tojson(objMsg)),
               'Error message should have included ' + tojson(objMsg));
});

tests.push(function assertShouldNotAcceptNonObjStringFunctionAsMsg() {
    const err = assert.throws(() => {
        assert(true, 1234);
    });

    assert.neq(-1, err.message.indexOf("msg parameter must be a "));
});

/* assert.eq tests */

tests.push(function eqShouldPassOnEquality() {
    assert.doesNotThrow(() => {
        assert.eq(3, 3);
    });
});

tests.push(function eqShouldFailWhenNotEqual() {
    assert.throws(() => {
        assert.eq(2, 3);
    });
});

tests.push(function eqShouldNotCallMsgFunctionOnSuccess() {
    var called = false;

    assert.doesNotThrow(() => {
        assert.eq(3, 3, () => {
            called = true;
        });
    });

    assert.eq(false, called, 'msg function should not have been called');
});

tests.push(function eqShouldCallMsgFunctionOnFailure() {
    var called = false;

    assert.throws(() => {
        assert.eq(1, 3, () => {
            called = true;
        });
    });

    assert.eq(true, called, 'msg function should have been called');
});

tests.push(function eqShouldPassOnObjectsWithSameContent() {
    const a = {'foo': true};
    const b = {'foo': true};

    assert.doesNotThrow(() => {
        assert.eq(a, b);
    }, [], 'eq should not throw exception on two objects with the same content');
});

/* assert.neq tests */

tests.push(function neqShouldFailOnEquality() {
    assert.throws(() => {
        assert.neq(3, 3);
    });
});

tests.push(function neqShouldPassWhenNotEqual() {
    assert.doesNotThrow(() => {
        assert.neq(2, 3);
    });
});

tests.push(function neqShouldFailOnObjectsWithSameContent() {
    const a = {'foo': true};
    const b = {'foo': true};

    assert.throws(() => {
        assert.neq(a, b);
    }, [], 'neq should throw exception on two objects with the same content');
});

/* assert.hasFields tests */

tests.push(function hasFieldsRequiresAnArrayOfFields() {
    const object = {field1: 1, field2: 1, field3: 1};

    assert.throws(() => {
        assert.hasFields(object, 'field1');
    });
});

tests.push(function hasFieldsShouldPassWhenObjectHasField() {
    const object = {field1: 1, field2: 1, field3: 1};

    assert.doesNotThrow(() => {
        assert.hasFields(object, ['field1']);
    });
});

tests.push(function hasFieldsShouldFailWhenObjectDoesNotHaveField() {
    const object = {field1: 1, field2: 1, field3: 1};

    assert.throws(() => {
        assert.hasFields(object, ['fieldDoesNotExist']);
    });
});

/* assert.contains tests */

tests.push(function containsShouldOnlyWorkOnArrays() {
    assert.throws(() => {
        assert.contains(42, 5);
    });
});

tests.push(function containsShouldPassIfArrayContainsValue() {
    const array = [1, 2, 3];

    assert.doesNotThrow(() => {
        assert.contains(2, array);
    });
});

tests.push(function containsShouldFailIfArrayDoesNotContainValue() {
    const array = [1, 2, 3];

    assert.throws(() => {
        assert.contains(42, array);
    });
});

/* assert.soon tests */

tests.push(function soonPassesWhenFunctionPasses() {
    assert.doesNotThrow(() => {
        assert.soon(() => {
            return true;
        });
    });
});

tests.push(function soonFailsIfMethodNeverPasses() {
    assert.throws(() => {
        assert.soon(() => {
            return false;
        }, 'assert message', kSmallTimeoutMS, kSmallRetryIntervalMS, {runHangAnalyzer: false});
    });
});

tests.push(function soonPassesIfMethodEventuallyPasses() {
    var count = 0;
    assert.doesNotThrow(() => {
        assert.soon(() => {
            count += 1;
            return count === 3;
        }, 'assert message', kDefaultTimeoutMS, kSmallRetryIntervalMS);
    });
});

/* assert.soonNoExcept tests */

tests.push(function soonNoExceptEventuallyPassesEvenWithExceptions() {
    var count = 0;
    assert.doesNotThrow(() => {
        assert.soonNoExcept(() => {
            count += 1;
            if (count < 3) {
                throw new Error('failed');
            }
            return true;
        }, 'assert message', kDefaultTimeoutMS, kSmallRetryIntervalMS);
    });
});

tests.push(function soonNoExceptFailsIfExceptionAlwaysThrown() {
    var count = 0;
    assert.throws(() => {
        assert.soonNoExcept(() => {
            throw new Error('failed');
        }, 'assert message', kSmallTimeoutMS, kSmallRetryIntervalMS, {runHangAnalyzer: false});
    });
});

/* assert.retry tests */

tests.push(function retryPassesAfterAFewAttempts() {
    var count = 0;

    assert.doesNotThrow(() => {
        assert.retry(() => {
            count += 1;
            return count === 3;
        }, 'assert message', kDefaultRetryAttempts, kSmallRetryIntervalMS);
    });
});

tests.push(function retryFailsAfterMaxAttempts() {
    assert.throws(() => {
        assert.retry(() => {
            return false;
        }, 'assert message', kDefaultRetryAttempts, kSmallRetryIntervalMS, {
            runHangAnalyzer: false
        });
    });
});

/* assert.retryNoExcept tests */

tests.push(function retryNoExceptPassesAfterAFewAttempts() {
    var count = 0;

    assert.doesNotThrow(() => {
        assert.retryNoExcept(() => {
            count += 1;
            if (count < 3) {
                throw new Error('failed');
            }
            return count === 3;
        }, 'assert message', kDefaultRetryAttempts, kSmallRetryIntervalMS);
    });
});

tests.push(function retryNoExceptFailsAfterMaxAttempts() {
    assert.throws(() => {
        assert.retryNoExcept(() => {
            throw new Error('failed');
        }, 'assert message', kDefaultRetryAttempts, kSmallRetryIntervalMS, {
            runHangAnalyzer: false
        });
    });
});

/* assert.time tests */

tests.push(function timeIsSuccessfulIfFuncExecutesInTime() {
    assert.doesNotThrow(() => {
        assert.time(() => {
            return true;
        }, 'assert message', kDefaultTimeoutMS);
    });
});

tests.push(function timeFailsIfFuncDoesNotFinishInTime() {
    assert.throws(() => {
        assert.time(() => {
            return true;
        }, 'assert message', -5 * 60 * 1000, {runHangAnalyzer: false});
    });
});

/* assert.isnull tests */

tests.push(function isnullPassesOnNull() {
    assert.doesNotThrow(() => {
        assert.isnull(null);
    });
});

tests.push(function isnullPassesOnUndefined() {
    assert.doesNotThrow(() => {
        assert.isnull(undefined);
    });
});

tests.push(function isnullFailsOnNotNull() {
    assert.throws(() => {
        assert.isnull('hello world');
    });
});

/* assert.lt tests */

tests.push(function ltPassesWhenLessThan() {
    assert.doesNotThrow(() => {
        assert.lt(3, 5);
    });
});

tests.push(function ltFailsWhenNotLessThan() {
    assert.throws(() => {
        assert.lt(5, 3);
    });
});

tests.push(function ltFailsWhenEqual() {
    assert.throws(() => {
        assert.lt(5, 5);
    });
});

tests.push(function ltPassesWhenLessThanWithTimestamps() {
    assert.doesNotThrow(() => {
        assert.lt(Timestamp(3, 0), Timestamp(10, 0));
    });
});

tests.push(function ltFailsWhenNotLessThanWithTimestamps() {
    assert.throws(() => {
        assert.lt(Timestamp(0, 10), Timestamp(0, 3));
    });
});

tests.push(function ltFailsWhenEqualWithTimestamps() {
    assert.throws(() => {
        assert.lt(Timestamp(5, 0), Timestamp(5, 0));
    });
});

/* assert.gt tests */

tests.push(function gtPassesWhenGreaterThan() {
    assert.doesNotThrow(() => {
        assert.gt(5, 3);
    });
});

tests.push(function gtFailsWhenNotGreaterThan() {
    assert.throws(() => {
        assert.gt(3, 5);
    });
});

tests.push(function gtFailsWhenEqual() {
    assert.throws(() => {
        assert.gt(5, 5);
    });
});

/* assert.lte tests */

tests.push(function ltePassesWhenLessThan() {
    assert.doesNotThrow(() => {
        assert.lte(3, 5);
    });
});

tests.push(function lteFailsWhenNotLessThan() {
    assert.throws(() => {
        assert.lte(5, 3);
    });
});

tests.push(function ltePassesWhenEqual() {
    assert.doesNotThrow(() => {
        assert.lte(5, 5);
    });
});

/* assert.gte tests */

tests.push(function gtePassesWhenGreaterThan() {
    assert.doesNotThrow(() => {
        assert.gte(5, 3);
    });
});

tests.push(function gteFailsWhenNotGreaterThan() {
    assert.throws(() => {
        assert.gte(3, 5);
    });
});

tests.push(function gtePassesWhenEqual() {
    assert.doesNotThrow(() => {
        assert.gte(5, 5);
    });
});

tests.push(function gtePassesWhenGreaterThanWithTimestamps() {
    assert.doesNotThrow(() => {
        assert.gte(Timestamp(0, 10), Timestamp(0, 3));
    });
});

tests.push(function gteFailsWhenNotGreaterThanWithTimestamps() {
    assert.throws(() => {
        assert.gte(Timestamp(0, 3), Timestamp(0, 10));
    });
});

tests.push(function gtePassesWhenEqualWIthTimestamps() {
    assert.doesNotThrow(() => {
        assert.gte(Timestamp(5, 0), Timestamp(5, 0));
    });
});

/* assert.betweenIn tests */

tests.push(function betweenInPassWhenNumberIsBetween() {
    assert.doesNotThrow(() => {
        assert.betweenIn(3, 4, 5);
    });
});

tests.push(function betweenInFailsWhenNumberIsNotBetween() {
    assert.throws(() => {
        assert.betweenIn(3, 5, 4);
    });
});

tests.push(function betweenInPassWhenNumbersEqual() {
    assert.doesNotThrow(() => {
        assert.betweenIn(3, 3, 5);
    });
    assert.doesNotThrow(() => {
        assert.betweenIn(3, 5, 5);
    });
});

/* assert.betweenEx tests */

tests.push(function betweenExPassWhenNumberIsBetween() {
    assert.doesNotThrow(() => {
        assert.betweenEx(3, 4, 5);
    });
});

tests.push(function betweenExFailsWhenNumberIsNotBetween() {
    assert.throws(() => {
        assert.betweenEx(3, 5, 4);
    });
});

tests.push(function betweenExFailsWhenNumbersEqual() {
    assert.throws(() => {
        assert.betweenEx(3, 3, 5);
    });
    assert.throws(() => {
        assert.betweenEx(3, 5, 5);
    });
});

/* assert.sameMembers tests */

tests.push(function sameMembersFailsWithInvalidArguments() {
    assert.throws(() => assert.sameMembers());
    assert.throws(() => assert.sameMembers([]));
    assert.throws(() => assert.sameMembers({}, {}));
    assert.throws(() => assert.sameMembers(1, 1));
});

tests.push(function sameMembersFailsWhenLengthsDifferent() {
    assert.throws(() => assert.sameMembers([], [1]));
    assert.throws(() => assert.sameMembers([], [1]));
    assert.throws(() => assert.sameMembers([1, 2], [1]));
    assert.throws(() => assert.sameMembers([1], [1, 2]));
});

tests.push(function sameMembersFailsWhenCountsOfDuplicatesDifferent() {
    assert.throws(() => assert.sameMembers([1, 1], [1, 2]));
    assert.throws(() => assert.sameMembers([1, 2], [1, 1]));
});

tests.push(function sameMembersFailsWithDifferentObjects() {
    assert.throws(() => assert.sameMembers([{_id: 0, a: 0}], [{_id: 0, a: 1}]));
    assert.throws(() => assert.sameMembers([{_id: 1, a: 0}], [{_id: 0, a: 0}]));
    assert.throws(() => {
        assert.sameMembers([{a: [{b: 0, c: 0}], _id: 0}], [{_id: 0, a: [{c: 0, b: 1}]}]);
    });
});

tests.push(function sameMembersFailsWithDifferentBSONTypes() {
    assert.throws(() => {
        assert.sameMembers([new BinData(0, "JANgqwetkqwklEWRbWERKKJREtbq")],
                           [new BinData(0, "xxxgqwetkqwklEWRbWERKKJREtbq")]);
    });
    assert.throws(() => assert.sameMembers([new Timestamp(0, 1)], [new Timestamp(0, 2)]));
});

tests.push(function sameMembersFailsWithCustomCompareFn() {
    const compareBinaryEqual = (a, b) => bsonBinaryEqual(a, b);
    assert.throws(() => {
        assert.sameMembers([NumberLong(1)], [1], undefined /*msg*/, compareBinaryEqual);
    });
    assert.throws(() => {
        assert.sameMembers([NumberLong(1), NumberInt(2)],
                           [2, NumberLong(1)],
                           undefined /*msg*/,
                           compareBinaryEqual);
    });
});

tests.push(function sameMembersDoesNotSortNestedArrays() {
    assert.throws(() => assert.sameMembers([[1, 2]], [[2, 1]]));
    assert.throws(() => {
        assert.sameMembers([{a: [{b: 0}, {b: 1, c: 0}], _id: 0}],
                           [{_id: 0, a: [{c: 0, b: 1}, {b: 0}]}]);
    });
});

tests.push(function sameMembersPassesWithEmptyArrays() {
    assert.sameMembers([], []);
});

tests.push(function sameMembersPassesSingleElement() {
    assert.sameMembers([1], [1]);
});

tests.push(function sameMembersPassesWithSameOrder() {
    assert.sameMembers([1, 2], [1, 2]);
    assert.sameMembers([1, 2, 3], [1, 2, 3]);
});

tests.push(function sameMembersPassesWithDifferentOrder() {
    assert.sameMembers([2, 1], [1, 2]);
    assert.sameMembers([1, 2, 3], [3, 1, 2]);
});

tests.push(function sameMembersPassesWithDuplicates() {
    assert.sameMembers([1, 1, 2], [1, 1, 2]);
    assert.sameMembers([1, 1, 2], [1, 2, 1]);
    assert.sameMembers([2, 1, 1], [1, 1, 2]);
});

tests.push(function sameMembersPassesWithSortedNestedArrays() {
    assert.sameMembers([[1, 2]], [[1, 2]]);
    assert.sameMembers([{a: [{b: 0}, {b: 1, c: 0}], _id: 0}],
                       [{_id: 0, a: [{b: 0}, {c: 0, b: 1}]}]);
});

tests.push(function sameMembersPassesWithObjects() {
    assert.sameMembers([{_id: 0, a: 0}], [{_id: 0, a: 0}]);
    assert.sameMembers([{_id: 0, a: 0}, {_id: 1}], [{_id: 0, a: 0}, {_id: 1}]);
    assert.sameMembers([{_id: 0, a: 0}, {_id: 1}], [{_id: 1}, {_id: 0, a: 0}]);
});

tests.push(function sameMembersPassesWithUnsortedObjects() {
    assert.sameMembers([{a: 0, _id: 1}], [{_id: 1, a: 0}]);
    assert.sameMembers([{a: [{b: 1, c: 0}], _id: 0}], [{_id: 0, a: [{c: 0, b: 1}]}]);
});

tests.push(function sameMembersPassesWithBSONTypes() {
    assert.sameMembers([new BinData(0, "JANgqwetkqwklEWRbWERKKJREtbq")],
                       [new BinData(0, "JANgqwetkqwklEWRbWERKKJREtbq")]);
    assert.sameMembers([new Timestamp(0, 1)], [new Timestamp(0, 1)]);
});

tests.push(function sameMembersPassesWithOtherTypes() {
    assert.sameMembers([null], [null]);
    assert.sameMembers([undefined], [undefined]);
    assert.sameMembers(["a"], ["a"]);
    assert.sameMembers([null, undefined, "a"], [undefined, "a", null]);
});

tests.push(function sameMembersDefaultCompareIsFriendly() {
    assert.sameMembers([NumberLong(1), NumberInt(2)], [2, 1]);
});

tests.push(function sameMembersPassesWithCustomCompareFn() {
    const compareBinaryEqual = (a, b) => bsonBinaryEqual(a, b);
    assert.sameMembers([[1, 2]], [[1, 2]], undefined /*msg*/, compareBinaryEqual);
    assert.sameMembers([NumberLong(1), NumberInt(2)],
                       [NumberInt(2), NumberLong(1)],
                       undefined /*msg*/,
                       compareBinaryEqual);
});

tests.push(function assertCallsHangAnalyzer() {
    function runAssertTest(f) {
        const oldMongoRunner = MongoRunner;
        let runs = 0;
        try {
            MongoRunner.runHangAnalyzer = function() {
                ++runs;
            };
            f();
            assert(false);
        } catch (e) {
            assert.eq(runs, 1);
        } finally {
            MongoRunner = oldMongoRunner;
        }
    }
    runAssertTest(
        () => assert.soon(() => false, 'assert message', kSmallTimeoutMS, kSmallRetryIntervalMS));
    runAssertTest(() => assert.retry(
                      () => false, 'assert message', kDefaultRetryAttempts, kSmallRetryIntervalMS));
    runAssertTest(() => assert.time(
                      () => sleep(5), 'assert message', 1 /* we certainly take less than this */));
});

/* main */

tests.forEach((test) => {
    jsTest.log(`Starting tests '${test.name}'`);
    test();
});
