// (c) Vitaliy Filippov, 2024
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1

const EtcTree = require('./etctree.js');
const AntiLocker = require('./antilocker.js');

const tests = {};

let cur_test = '';

const expect = (a, b) =>
{
    if (!EtcTree.eq(a, b))
    {
        process.stderr.write(cur_test+' test:\nexpected: '+JSON.stringify(b)+'\nreal:     '+JSON.stringify(a)+'\n'+new Error().stack.replace(/^.*\n.*\n/, '')+'\n');
        process.exit(1);
    }
};

tests['read/write'] = async () =>
{
    const t = new EtcTree();
    expect(
        await t.api_txn({ success: [ { request_put: { key: '/vitastor//config/global', value: { hello: 'world' } } } ] }),
        { header: { revision: 1 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(
        await t.api_txn({ success: [ { request_range: { key: '/vitastor/config/global' } } ] }),
        { header: { revision: 1 }, succeeded: true, responses: [ { response_range: {
            kvs: [ { key: '/vitastor/config/global', mod_revision: 1, value: { hello: 'world' } } ],
        } } ] }
    );
    expect(
        await t.api_txn({ success: [ { request_range: { key: '/vitastor/config/', range_end: '/vitastor/config0' } } ] }),
        { header: { revision: 1 }, succeeded: true, responses: [ { response_range: {
            kvs: [ { key: '/vitastor/config/global', mod_revision: 1, value: { hello: 'world' } } ],
        } } ] }
    );
    expect(
        await t.api_txn({ success: [ { request_range: { key: '/vitasto/', range_end: '/vitasto0' } } ] }),
        { header: { revision: 1 }, succeeded: true, responses: [ { response_range: { kvs: [] } } ] }
    );
    expect(
        await t.api_txn({
            compare: [ { key: '/vitastor/config/global', target: 'MOD', mod_revision: 1, result: 'LESS' } ],
            success: [ { request_put: { key: '/vitastor//config/global', value: { hello: 'world' } } } ],
            failure: [ { request_range: { key: '/vitastor/config/global' } } ],
        }),
        { header: { revision: 1 }, succeeded: false, responses: [ { response_range: {
            kvs: [ { key: '/vitastor/config/global', mod_revision: 1, value: { hello: 'world' } } ],
        } } ] }
    );
    expect(
        await t.api_txn({
            compare: [ { key: '/vitastor/config/global', target: 'MOD', mod_revision: 2, result: 'LESS' } ],
            success: [ { request_put: { key: '/vitastor//config/global', value: { hello: 'world2' } } } ]
        }),
        { header: { revision: 2 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(
        await t.api_txn({ success: [ { request_range: { key: '/vitastor/config/', range_end: '/vitastor/config0' } } ] }),
        { header: { revision: 2 }, succeeded: true, responses: [ { response_range: {
            kvs: [ { key: '/vitastor/config/global', mod_revision: 2, value: { hello: 'world2' } } ],
        } } ] }
    );
    expect(
        t.dump(false),
        {"state":{"children":{"":{"children":{"vitastor":{"children":{"config":{"children":{"global":{"version":2,"mod_revision":2,"create_revision":1,"value":{"hello":"world2"}}}}}}}}}},"mod_revision":2,"compact_revision":0,"leases":{}}
    );
    t.destroy();
};

tests['watch'] = async () =>
{
    const t = new EtcTree();
    const sent = [];
    const send = (event) => sent.push(event);
    expect(
        await t.api_txn({ success: [ { request_put: { key: '/vitastor//config/global', value: { hello: 'world' } } } ] }),
        { header: { revision: 1 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(
        t.api_create_watch({ watch_id: 1, key: '/vitastor/', range_end: '/vitastor0' }, send),
        { header: { revision: 1 }, watch_id: 1, created: true }
    );
    expect(sent, []);
    expect(
        await t.api_txn({ success: [ { request_put: { key: '/vitastor/osd/state/1', value: { ip: '1.2.3.4' } } } ] }),
        { header: { revision: 2 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(sent, [ { result: { header: { revision: 2 }, events: [ { type: 'PUT', kv: { key: '/vitastor/osd/state/1', value: { ip: '1.2.3.4' }, mod_revision: 2 } } ] } } ]);
    t.destroy();
};

tests['merge watch'] = async () =>
{
    const t = new EtcTree();
    const sent = [];
    const send = (event) => sent.push(event);
    expect(
        await t.api_txn({ success: [ { request_put: { key: '/vitastor//config/pgs', value: { items: {} } } } ] }),
        { header: { revision: 1 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(
        t.api_create_watch({ watch_id: 1, key: '/vitastor/config/pgs' }, send, 'X1' /* stream_id */),
        { header: { revision: 1 }, watch_id: 1, created: true }
    );
    expect(
        t.api_create_watch({ watch_id: 2, key: '/vitastor/pg/history/', range_end: '/vitastor/pg/history0' }, send, 'X1' /* stream_id */),
        { header: { revision: 1 }, watch_id: 2, created: true },
    );
    expect(sent, []);
    expect(
        await t.api_txn({ success: [
            { request_put: { key: '/vitastor/config/pgs', value: { items: { 1: { 1: { osd_set: [ 1, 2, 3 ] } } } } } },
            { request_put: { key: '/vitastor/pg/history/1/1', value: { all_peers: [ 1, 2, 3, 4, 5 ] } } },
        ] }),
        { header: { revision: 2 }, succeeded: true, responses: [ { response_put: {} }, { response_put: {} } ] }
    );
    expect(sent, [ { result: { header: { revision: 2 }, events: [
        { type: 'PUT', kv: { key: '/vitastor/config/pgs', value: { items: { 1: { 1: { osd_set: [ 1, 2, 3 ] } } } }, mod_revision: 2 } },
        { type: 'PUT', kv: { key: '/vitastor/pg/history/1/1', value: { all_peers: [ 1, 2, 3, 4, 5 ] }, mod_revision: 2 } },
    ] } } ]);
    t.destroy();
};

tests['lease'] = async () =>
{
    const t = new EtcTree();
    const sent = [];
    const send = (event) => sent.push(event);
    const leaseID = (await t.api_grant_lease({ TTL: 0.5 })).ID;
    expect(leaseID != null, true);
    expect(
        await t.api_txn({ success: [ { request_put: { key: '/vitastor/osd/state/1', lease: leaseID, value: { ip: '1.2.3.4' } } } ] }),
        { header: { revision: 2 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(
        t.api_create_watch({ watch_id: 1, key: '/vitastor/', range_end: '/vitastor0' }, send),
        { header: { revision: 2 }, watch_id: 1, created: true }
    );
    expect(sent, []);
    const dump = t.dump(false);
    const expires = dump.leases[leaseID].expires;
    expect(dump, {"state":{"children":{"":{"children":{"vitastor":{"children":{"osd":{"children":{"state":{"children":{"1":{"lease":leaseID,"version":1,"mod_revision":2,"create_revision":2,"value":{"ip":"1.2.3.4"}}}}}}}}}}}},"mod_revision":2,"compact_revision":0,"leases":{[leaseID]:{"ttl":0.5,"expires":expires}}});
    await new Promise(ok => setTimeout(ok, 600));
    expect(sent, [ { result: { header: { revision: 3 }, events: [ { type: 'DELETE', kv: { key: '/vitastor/osd/state/1', mod_revision: 3 } } ] } } ]);
    t.pause_leases();
    t.load(dump);
    expect(t.dump(false), dump);
    const t2 = new EtcTree();
    t2.pause_leases();
    t2.load(dump);
    expect(t2.dump(false), dump);
    t.destroy();
    t2.destroy();
};

tests['update'] = async () =>
{
    const t1 = new EtcTree();
    const t2 = new EtcTree();
    const leaseID = (await t1.api_grant_lease({ TTL: 0.5 })).ID;
    expect(leaseID != null, true);
    expect(
        await t1.api_txn({ success: [ { request_put: { key: '/vitastor/osd/state/1', lease: leaseID, value: { ip: '1.2.3.4' } } } ] }),
        { header: { revision: 2 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(
        await t2.api_txn({ success: [ { request_put: { key: '/vitastor/osd/state/1', value: { ip: '1.2.3.6' } } } ] }),
        { header: { revision: 1 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(
        await t1.api_txn({ success: [ { request_put: { key: '/vitastor/osd/state/1', lease: leaseID, value: { ip: '1.2.3.5' } } } ] }),
        { header: { revision: 3 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    let dump2 = t2.dump();
    t2.load(t1.dump(), true);
    t1.load(dump2, true);
    let dump = t2.dump(false);
    let expires = dump.leases[leaseID].expires;
    expect(dump, {"state":{"children":{"":{"children":{"vitastor":{"children":{"osd":{"children":{"state":{"children":{"1":{"lease":leaseID,"version":2,"mod_revision":3,"create_revision":2,"value":{"ip":"1.2.3.5"}}}}}}}}}}}},"mod_revision":3,"compact_revision":0,"leases":{[leaseID]:{"ttl":0.5,"expires":expires}}});
    expect(t1.dump(false), {"state":{"children":{"":{"children":{"vitastor":{"children":{"osd":{"children":{"state":{"children":{"1":{"lease":leaseID,"version":2,"mod_revision":3,"create_revision":2,"value":{"ip":"1.2.3.5"}}}}}}}}}}}},"mod_revision":3,"compact_revision":0,"leases":{[leaseID]:{"ttl":0.5,"expires":expires}}});
    t1.destroy();
    t2.destroy();
};

tests['dump filter'] = async () =>
{
    const t1 = new EtcTree();
    const leaseID = (await t1.api_grant_lease({ TTL: 0.5 })).ID;
    expect(leaseID != null, true);
    expect(
        await t1.api_txn({ success: [
            { request_put: { key: '/vitastor/osd/state/1', lease: leaseID, value: { ip: '1.2.3.6' } } },
            { request_put: { key: '/vitastor/osd/stats/1', value: { time: 123, read: [ 10, 1000, 10459 ] } } },
        ] }),
        { header: { revision: 2 }, succeeded: true, responses: [ { response_put: {} }, { response_put: {} } ] }
    );
    expect(
        t1.dump(true, (key, value) => (key.substr(0, '/vitastor/osd/stats'.length) === '/vitastor/osd/stats' ? { time: value.time } : value)),
        {"state":{"children":{"":{"children":{"vitastor":{"children":{"osd":{"children":{"stats":{"children":{"1":{"version":1,"mod_revision":2,"create_revision":2,"value":{"time":123}}}}}}}}}}}},"mod_revision":2,"compact_revision":0}
    );
    t1.destroy();
};

tests['replicate watcher'] = async () =>
{
    const t = new EtcTree();
    t.set_replicate_watcher(async () =>
    {
        throw new Error('replication failed');
    });
    let thrown = false;
    try
    {
        await t.api_txn({ success: [ { request_put: { key: '/vitastor/osd/state/1', value: { ip: '1.2.3.4' } } } ] });
    }
    catch (e)
    {
        thrown = e;
    }
    expect(thrown && thrown.message == 'replication failed', true);
    t.destroy();
};

tests['parallel txn'] = async () =>
{
    const t = new EtcTree();
    const cbs = [];
    t.set_replicate_watcher(() =>
    {
        return new Promise(ok => cbs.push(ok));
    });
    const p1 = t.api_txn({ success: [ { request_range: { key: 'abc' } }, { request_put: { key: 'def', value: '1' } } ] });
    const p2 = t.api_txn({ success: [ { request_put: { key: 'abc', value: '2' } } ] });
    expect(cbs.length, 2);
    cbs[1]();
    cbs[0]();
    expect(await p1, { header: { revision: 1 }, succeeded: true, responses: [ { response_range: { kvs: [] } }, { response_put: {} } ] });
    expect(await p2, { header: { revision: 2 }, succeeded: true, responses: [ { response_put: {} } ] });
    t.destroy();
};

tests['delete_compare'] = async () =>
{
    const t = new EtcTree();
    expect(
        await t.api_txn({ success: [ { request_put: { key: 'abc', value: 'def' } } ] }),
        { header: { revision: 1 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    expect(
        await t.api_txn({ success: [ { request_delete_range: { key: 'abc' } } ] }),
        { header: { revision: 2 }, succeeded: true, responses: [ { response_delete_range: { deleted: 1 } } ] }
    );
    expect(
        await t.api_txn({
            compare: [ { key: 'abc', target: 'MOD', mod_revision: 2, result: 'LESS' } ],
            success: [ { request_put: { key: 'abc', value: 'zzz' } } ],
        }),
        { header: { revision: 2 }, succeeded: false, responses: [] }
    );
    expect(
        await t.api_txn({
            compare: [ { key: 'def', target: 'MOD', mod_revision: 2, result: 'LESS' } ],
            success: [ { request_put: { key: 'def', value: 'xxx' } } ],
        }),
        { header: { revision: 3 }, succeeded: true, responses: [ { response_put: {} } ] }
    );
    t.destroy();
};

tests['txn_locks'] = async () =>
{
    const t = new AntiLocker({});
    t.set_txn_locks([ { key: 'abc', value: 'def' } ], 1);
    expect(
        t.check_txn_locks({ success: [ { request_range: { key: 'abc' } } ] }),
        1
    );
    let done = false;
    t.wait_commit(1, Date.now()+10000).then(() => { done = true; });
    await new Promise(ok => setTimeout(ok, 50));
    expect(done, false);
    t.unlock_txn(1);
    await new Promise(ok => setTimeout(ok, 50));
    expect(done, true);
    t.destroy();
};

(async function()
{
    for (cur_test in tests)
    {
        await tests[cur_test]();
        console.log(cur_test+' test: OK');
    }
})().catch(console.error);
