// Verifies the serverStatus output and FTDC output for the read write concern defaults.
//
// @tags: [requires_sharding]
import {verifyGetDiagnosticData} from "jstests/libs/ftdc.js";
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";

const Topology = {
    Shard: "shard",
    MongoS: "mongos",
    Config: "config",
    Standalone: "standalone",
    ReplicaSet: "rs",
};

// Verifies the transaction server status response has the fields that we expect.
function verifyServerStatus(conn,
                            {expectedRC, expectedWC, expectNoDefaultsDocument, expectNothing},
                            isImplicitDefaultWCMajority) {
    const res = assert.commandWorked(conn.adminCommand({serverStatus: 1}));
    if (expectNothing) {
        assert.eq(undefined, res.defaultRWConcern, tojson(res.defaultRWConcern));
        return;
    }

    assert.hasFields(res, ["defaultRWConcern"]);
    const defaultsRes = res.defaultRWConcern;

    assert.hasFields(defaultsRes, ["defaultReadConcernSource"]);
    if (!expectedRC) {
        assert.eq("implicit", defaultsRes.defaultReadConcernSource, tojson(defaultsRes));
        expectedRC = {level: "local"};
    } else {
        assert.eq("global", defaultsRes.defaultReadConcernSource, tojson(defaultsRes));
    }

    if (expectedRC) {
        assert.eq(expectedRC, defaultsRes.defaultReadConcern, tojson(defaultsRes));
    } else {
        assert.eq(undefined, defaultsRes.defaultReadConcern, tojson(defaultsRes));
    }

    assert.hasFields(defaultsRes, ["defaultWriteConcernSource"]);
    if (!expectedWC) {
        assert.eq("implicit", defaultsRes.defaultWriteConcernSource, tojson(defaultsRes));
        if (isImplicitDefaultWCMajority) {
            expectedWC = {w: "majority", wtimeout: 0};
        }
    } else {
        assert.eq("global", defaultsRes.defaultWriteConcernSource, tojson(defaultsRes));
    }

    if (expectedWC) {
        assert.eq(expectedWC, defaultsRes.defaultWriteConcern, tojson(defaultsRes));
    } else {
        assert.eq(undefined, defaultsRes.defaultWriteConcern, tojson(defaultsRes));
    }

    if (expectNoDefaultsDocument) {
        // When defaults have never been set (or the defaults document was deleted) the response
        // should only contain localUpdateWallClockTime.
        assert.hasFields(defaultsRes, ["localUpdateWallClockTime"]);
        assert.eq(undefined, defaultsRes.updateWallClockTime, tojson(defaultsRes));
        assert.eq(undefined, defaultsRes.updateOpTime, tojson(defaultsRes));
    } else {
        // These fields are always set once a read or write concern has been set at least once and
        // the defaults document is still present.
        assert.hasFields(defaultsRes,
                         ["updateOpTime", "updateWallClockTime", "localUpdateWallClockTime"]);
    }
}

function testServerStatus(conn, isImplicitDefaultWCMajority) {
    // When no defaults have been set.
    verifyServerStatus(conn, {expectNoDefaultsDocument: true}, isImplicitDefaultWCMajority);

    // When only read concern is set.
    assert.commandWorked(
        conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "majority"}}));
    verifyServerStatus(conn, {expectedRC: {level: "majority"}}, isImplicitDefaultWCMajority);

    // When read concern is explicitly unset and write concern is unset.
    assert.commandWorked(conn.adminCommand(
        {setDefaultRWConcern: 1, defaultReadConcern: {}, defaultWriteConcern: {}}));
    verifyServerStatus(conn, {}, isImplicitDefaultWCMajority);

    // When only write concern is set.
    assert.commandWorked(conn.adminCommand(
        {setDefaultRWConcern: 1, defaultReadConcern: {}, defaultWriteConcern: {w: "majority"}}));
    verifyServerStatus(
        conn, {expectedWC: {w: "majority", wtimeout: 0}}, isImplicitDefaultWCMajority);

    // When both read and write concern are set.
    assert.commandWorked(conn.adminCommand({
        setDefaultRWConcern: 1,
        defaultReadConcern: {level: "majority"},
        defaultWriteConcern: {w: "majority"}
    }));
    verifyServerStatus(conn,
                       {expectedRC: {level: "majority"}, expectedWC: {w: "majority", wtimeout: 0}},
                       isImplicitDefaultWCMajority);

    // When the defaults document has been deleted.
    assert.commandWorked(conn.getDB("config").settings.remove({_id: "ReadWriteConcernDefaults"}));
    assert.soon(() => {
        // Wait for the cache to discover the defaults were deleted. Note the cache is invalidated
        // on delete on a mongod, so this is only to handle the mongos case.
        const res = conn.adminCommand({getDefaultRWConcern: 1, inMemory: true});
        return !res.hasOwnProperty("updateOpTime");
    }, "mongos failed to pick up deleted default rwc", undefined, 1000, {runHangAnalyzer: false});
    verifyServerStatus(conn, {expectNoDefaultsDocument: true}, isImplicitDefaultWCMajority);
}

function testFTDC(conn, topology, ftdcDirPath, expectNothingOnRotation = false) {
    //
    // The periodic samples used for FTDC shouldn't include the default read write concerns.
    //
    let latestPeriodicFTDC = verifyGetDiagnosticData(conn.getDB("admin"));

    if (topology === Topology.MongoS || topology === Topology.Shard ||
        topology === Topology.Config) {
        latestPeriodicFTDC = latestPeriodicFTDC.common;
    }

    assert.hasFields(latestPeriodicFTDC, ["serverStatus"]);
    assert.eq(undefined,
              latestPeriodicFTDC.serverStatus.defaultRWConcern,
              tojson(latestPeriodicFTDC.serverStatus));

    //
    // The first sample in the FTDC file should have the default read write concern, if the node is
    // expected to collect it on log file rotation.
    //

    const ftdcFiles = listFiles(ftdcDirPath);

    // Read from the first non-interim file.
    const firstFullFile =
        ftdcFiles.filter(fileDesc => fileDesc.baseName.indexOf("interim") == -1)[0];
    var ftdcData;
    assert.soon(() => {
        try {
            ftdcData = _readDumpFile(firstFullFile.name);
            return true;
        } catch (error) {
            jsTestLog(`Caught unexpected error: ${tojson(error)}`);
            return false;
        }
    });
    assert.hasFields(ftdcData[0], ["doc"], tojson(ftdcData));

    // Look for the defaults in the first metadata object.
    const firstMetadataObj = ftdcData.filter(obj => obj.type == 0)[0];
    assert.hasFields(firstMetadataObj, ["doc"]);

    if (expectNothingOnRotation) {
        assert.eq(undefined, firstMetadataObj.doc.getDefaultRWConcern, tojson(ftdcData[0].doc));
        if (topology === Topology.Shard) {
            assert.eq(undefined,
                      firstMetadataObj.doc.common.getDefaultRWConcern,
                      tojson(ftdcData[0].doc));
        }
    } else {
        let getDefaultRWConcernDoc = {};
        if (topology === Topology.ReplicaSet || topology === Topology.Standalone) {
            getDefaultRWConcernDoc = firstMetadataObj.doc;
        } else {
            getDefaultRWConcernDoc = firstMetadataObj.doc.common;
        }
        assert.hasFields(getDefaultRWConcernDoc, ["getDefaultRWConcern"]);
    }
}

jsTestLog("Testing sharded cluster...");
{
    const testPathMongos = MongoRunner.toRealPath("ftdc_dir_mongos");
    const testPathConfig = MongoRunner.toRealPath("ftdc_dir_config");
    const testPathShard = MongoRunner.toRealPath("ftdc_dir_shard");
    const st = new ShardingTest({
        shards: [{setParameter: {diagnosticDataCollectionDirectoryPath: testPathShard}}],
        mongos: {
            s0: {setParameter: {diagnosticDataCollectionDirectoryPath: testPathMongos}},
        },
        config: 1,
        configOptions: {setParameter: {diagnosticDataCollectionDirectoryPath: testPathConfig}}
    });

    testServerStatus(st.s, true /* isImplicitDefaultWCMajority */);
    testFTDC(st.s, Topology.MongoS, testPathMongos);
    testServerStatus(st.configRS.getPrimary(), true /* isImplicitDefaultWCMajority */);
    testFTDC(st.configRS.getPrimary(), Topology.Shard, testPathConfig);

    // Set a default before verifying it isn't included by shards.
    assert.commandWorked(
        st.s.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: "majority"}}));
    verifyServerStatus(
        st.rs0.getPrimary(), {expectNothing: true}, true /* isImplicitDefaultWCMajority */);
    testFTDC(
        st.rs0.getPrimary(), Topology.Shard, testPathShard, true /* expectNothingOnRotation */);
    st.stop();
}

jsTestLog("Testing plain replica set...");
{
    const testPath = MongoRunner.toRealPath("ftdc_dir_repl_node");
    const rst = new ReplSetTest(
        {nodes: 1, nodeOptions: {setParameter: {diagnosticDataCollectionDirectoryPath: testPath}}});
    rst.startSet();
    rst.initiate();

    testServerStatus(rst.getPrimary(), true /* isImplicitDefaultWCMajority */);
    testFTDC(rst.getPrimary(), Topology.ReplicaSet, testPath);

    rst.stopSet();
}

jsTestLog("Testing server status for plain replica set with implicit default WC {w:1}");
{
    const rst = new ReplSetTest({nodes: [{}, {}, {arbiter: true}]});
    rst.startSet();
    rst.initiate();

    testServerStatus(rst.getPrimary(), false /* isImplicitDefaultWCMajority */);
    rst.stopSet();
}

jsTestLog("Testing standalone server...");
{
    const testPath = MongoRunner.toRealPath("ftdc_dir_standalone");
    const standalone =
        MongoRunner.runMongod({setParameter: {diagnosticDataCollectionDirectoryPath: testPath}});

    verifyServerStatus(standalone, {expectNothing: true}, true /* isImplicitDefaultWCMajority */);
    testFTDC(standalone, Topology.Standalone, testPath, true /* expectNothingOnRotation */);

    MongoRunner.stopMongod(standalone);
}
