/**
 * Tests TTL indexes with invalid values for 'expireAfterSeconds'.
 *
 * @tags: [
 *     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";

function test(expireAfterSecondsVal) {
    const disabledValue = 2147483647;

    jsTestLog("Testing expireAfterSeconds = " + expireAfterSecondsVal);

    const rst = new ReplSetTest({
        nodes: [{}, {rsConfig: {votes: 0, priority: 0}}],
        nodeOptions: {setParameter: {ttlMonitorSleepSecs: 5}},
        // Sync from primary only so that we have a well-defined node to check listIndexes behavior.
        settings: {chainingAllowed: false},
    });
    rst.startSet();
    rst.initiate(null, null, {initiateWithDefaultElectionTimeout: true});

    let primary = rst.getPrimary();
    const db = primary.getDB('test');
    const coll = db.t;

    // Insert a document before creating the index. Index builds on empty collections skip the
    // collection scan phase, which we look for using checkLog below.
    assert.commandWorked(coll.insert({_id: 0, t: ISODate()}));

    // The test cases here revolve around having a TTL index in the catalog with an invalid
    // 'expireAfterSeconds'. The current createIndexes behavior will reject index creation for
    // invalid values of expireAfterSeconds, so we use a failpoint to disable that checking to
    // simulate a value leftover from very old MongoDB versions.
    const fp = configureFailPoint(primary, 'skipTTLIndexValidationOnCreateIndex');
    const fp2 = configureFailPoint(primary, 'skipTTLIndexExpireAfterSecondsValidation');
    try {
        assert.commandWorked(coll.createIndex({t: 1}, {expireAfterSeconds: expireAfterSecondsVal}));
    } finally {
        fp.off();
        fp2.off();
    }

    // Log the contents of the catalog for debugging purposes in case of failure.
    let catalogContents = coll.aggregate([{$listCatalog: {}}]).toArray();
    jsTestLog("Catalog contents: " + tojson(catalogContents));

    // Wait for "Skipping TTL job due to invalid index spec" log message.
    checkLog.containsJson(primary, 6909100, {ns: coll.getFullName()});

    // TTL index should be replicated to the secondary with an invalid 'expireAfterSeconds'.
    const secondary = rst.getSecondary();
    checkLog.containsJson(secondary, 20384, {
        namespace: coll.getFullName(),
        properties: (spec) => {
            jsTestLog('TTL index on secondary: ' + tojson(spec));
            // NaN does not equal NaN, so we have to use the isNaN function for this check.
            if (isNaN(expireAfterSecondsVal))
                return isNaN(spec.expireAfterSeconds);
            else
                return spec.expireAfterSeconds == expireAfterSecondsVal;
        }
    });

    assert.eq(coll.countDocuments({}),
              1,
              'ttl index with invalid expireAfterSeconds should not remove any documents.');

    // Confirm that TTL index is replicated with a non-zero 'expireAfterSeconds' during initial
    // sync.
    const newNode = rst.add({rsConfig: {votes: 0, priority: 0}});
    rst.reInitiate();
    rst.waitForState(newNode, ReplSetTest.State.SECONDARY);
    rst.awaitReplication();
    let newNodeTestDB = newNode.getDB(db.getName());
    let newNodeColl = newNodeTestDB.getCollection(coll.getName());
    const newNodeIndexes = IndexBuildTest.assertIndexes(newNodeColl, 2, ['_id_', 't_1']);
    const newNodeSpec = newNodeIndexes.t_1;
    jsTestLog('TTL index on initial sync node: ' + tojson(newNodeSpec));
    assert(newNodeSpec.hasOwnProperty('expireAfterSeconds'),
           'Index was not replicated as a TTL index during initial sync.');
    assert.eq(
        newNodeSpec.expireAfterSeconds,
        disabledValue,
        expireAfterSecondsVal +
            ' expireAferSeconds was replicated as something other than disabled during initial sync.');

    // Check that listIndexes on the primary logged a "Fixing expire field from TTL index spec"
    // message during the invalid 'expireAfterSeconds' conversion.
    checkLog.containsJson(primary, 6835900, {namespace: coll.getFullName()});

    // Confirm that a node with an existing TTL index with an invalid 'expireAfterSeconds' will
    // convert the duration on the TTL index from the invalid value to a large positive value when
    // it becomes the primary node. When stepping down the primary, we use 'force' because there's
    // no other electable node.  Subsequently, we wait for the stepped down node to become primary
    // again.  To confirm that the TTL index has been fixed, we check the oplog for a collMod
    // operation on the TTL index that changes the `expireAfterSeconds` field from the invalid value
    // to a large positive value.
    assert.commandWorked(primary.adminCommand({replSetStepDown: 5, force: true}));
    primary = rst.waitForPrimary();

    // Log the contents of the catalog for debugging purposes in case of failure.
    let newPrimaryColl = primary.getDB(db.getName()).getCollection(coll.getName());
    const newPrimaryCatalogContents = newPrimaryColl.aggregate([{$listCatalog: {}}]).toArray();
    jsTestLog("Catalog contents on new primary: " + tojson(newPrimaryCatalogContents));

    assert.soon(
        () => {
            return 1 ==
                rst.findOplog(primary,
                              {
                                  op: 'c',
                                  ns: newPrimaryColl.getDB().getCollection('$cmd').getFullName(),
                                  'o.collMod': coll.getName(),
                                  'o.index.name': 't_1',
                                  'o.index.expireAfterSeconds': newNodeSpec.expireAfterSeconds
                              },
                              /*limit=*/ 1)
                    .toArray()
                    .length;
        },
        'TTL index with ' + expireAfterSecondsVal +
            ' expireAfterSeconds was not fixed using collMod during step-up: ' +
            tojson(rst.findOplog(primary, {op: {$ne: 'n'}}, /*limit=*/ 10).toArray()));

    const catalog = coll.aggregate([{$listCatalog: {}}]).toArray();
    assert.eq(catalog[0].db, db.getName());
    assert.eq(catalog[0].name, coll.getName());
    assert.eq(catalog[0].md.indexes.length, 2);
    assert.eq(catalog[0].md.indexes[0].spec.name, "_id_");
    assert.eq(catalog[0].md.indexes[1].spec.expireAfterSeconds, disabledValue);

    rst.stopSet();
}

test(NaN);
const maxDouble = 1.7976931348623157e+308;
test(maxDouble);
test(-maxDouble);
