/*
 * Copyright (C) 2015 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

import cockpit from "cockpit";
import QUnit, { skipWithPybridge } from "qunit-tests";

function deep_update(target, data) {
    for (const prop in data) {
        if (Object.prototype.toString.call(data[prop]) === '[object Object]') {
            if (!target[prop])
                target[prop] = {};
            deep_update(target[prop], data[prop]);
        } else {
            target[prop] = data[prop];
        }
    }
}

export function common_dbus_tests(channel_options, bus_name) { // eslint-disable-line no-unused-vars
    QUnit.test("call method", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        assert.equal(typeof dbus.call, "function", "is a function");
        const reply = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
                                      "HelloWorld", ["Browser-side JS"]);
        assert.deepEqual(reply, ["Word! You said `Browser-side JS'. I'm Skeleton, btw!"], "reply");
    });

    QUnit.test("call method with timeout", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "NeverReturn",
                            [], { timeout: 10 });
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.ok([
                "org.freedesktop.DBus.Error.Timeout",
                "org.freedesktop.DBus.Error.NoReply"
            ].indexOf(ex.name) >= 0);
        }
    });

    QUnit.test("close immediately", assert => {
        const done = assert.async();
        assert.expect(1);
        const dbus = cockpit.dbus(bus_name, channel_options);
        dbus.addEventListener("close", (_event, options) => {
            assert.equal(options.problem, "test-code", "got right code");
            done();
        });

        window.setTimeout(() => dbus.close("test-code"), 100);
    });

    QUnit.test("call close", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            const promise = dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"]);
            dbus.close();
            await promise;
            // assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.problem, "disconnected", "got right close code");
        }
    });

    QUnit.test("call closed", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        dbus.close("blah-blah");

        try {
            await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
                            "HelloWorld", ["Browser-side JS"]);
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.problem, "blah-blah", "got right close code");
        }
    });

    QUnit.test("primitive types", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        const reply = await dbus.call(
            "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TestPrimitiveTypes",
            [10, true, 11, 12, 13, 14, 15, 16, 17, "a string", "/a/path", "asig", "ZWZnAA=="]);
        assert.deepEqual(reply, [
            20, false, 111, 1012, 10013, 100014, 1000015, 10000016, 17.0 / Math.PI,
            "Word! You said `a string'. Rock'n'roll!", "/modified/a/path", "assgitasig",
            "Ynl0ZXN0cmluZyH/AA=="
        ], "round trip");
    });

    skipWithPybridge("integer bounds", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);

        async function testNumber(type, value, valid) {
            try {
                await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
                                "TestVariant", [{ t: type, v: value }]);
                if (!valid)
                    assert.ok(false, "should not be reached");
            } catch (ex) {
                assert.equal(valid, false);
                assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs");
            }
        }

        await testNumber('y', 0, true);
        await testNumber('y', 0xff, true);
        await testNumber('y', -1, false);
        await testNumber('y', 0xff + 1, false);
        await testNumber('n', -300, true);
        await testNumber('n', 300, true);
        await testNumber('n', -0x8000 - 1, false);
        await testNumber('n', 0x7fff + 1, false);
        await testNumber('q', 0, true);
        await testNumber('q', 300, true);
        await testNumber('q', -1, false);
        await testNumber('q', 0xffff + 1, false);
        await testNumber('i', -0xfffff, true);
        await testNumber('i', 0xfffff, true);
        await testNumber('i', -0x80000000 - 1, false);
        await testNumber('i', 0x7fffffff + 1, false);
        await testNumber('u', 0, true);
        await testNumber('u', 0xfffff, true);
        await testNumber('u', -1, false);
        await testNumber('u', 0xffffffff + 1, false);
        await testNumber('x', -0xfffffffff, true);
        await testNumber('x', 0xfffffffff, true);
        await testNumber('t', 0xfffffffff, true);
        await testNumber('t', -1, false);
    });

    QUnit.test("non-primitive types", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        const reply = await dbus.call(
            "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
            "TestNonPrimitiveTypes", [
                { one: "red", two: "blue" },
                { first: [42, 42], second: [43, 43] },
                [42, 'foo', 'bar'],
                ["one", "two"],
                ["/one", "/one/two"],
                ["ass", "git"],
                ["QUIA", "QkMA"]
            ]);
        assert.deepEqual(reply, [
            "{'one': 'red', 'two': 'blue'}{'first': (42, 42), 'second': (43, 43)}(42, 'foo', 'bar')array_of_strings: [one, two] array_of_objpaths: [/one, /one/two] array_of_signatures: [signature 'ass', 'git'] array_of_bytestrings: [AB, BC] "
        ], "round trip");
    });

    skipWithPybridge("variants", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        const reply = await dbus.call(
            "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
            "TestAsv", [{
                one: cockpit.variant("s", "foo"),
                two: cockpit.variant("o", "/bar"),
                three: cockpit.variant("g", "assgit"),
                four: cockpit.variant("y", 42),
                five: cockpit.variant("d", 1000.0)
            }]);
        assert.deepEqual(reply, [
            "{'one': <'foo'>, 'two': <objectpath '/bar'>, 'three': <signature 'assgit'>, 'four': <byte 0x2a>, 'five': <1000.0>}"
        ], "round trip");
    });

    skipWithPybridge("bad variants", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call(
                "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TestAsv",
                [{
                    one: "foo",
                    two: "/bar",
                    three: "assgit",
                    four: 42,
                    five: 1000.0
                }]);
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
            assert.equal(ex.message, "Unexpected type 'string' in argument", "error message");
        }
    });

    QUnit.test("get all", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        const reply = await dbus.call("/otree/frobber", "org.freedesktop.DBus.Properties", "GetAll",
                                      ["com.redhat.Cockpit.DBusTests.Frobber"]);
        assert.deepEqual(reply, [{
            FinallyNormalName: { t: "s", v: "There aint no place like home" },
            ReadonlyProperty: { t: "s", v: "blah" },
            aay: { t: "aay", v: [] },
            ag: { t: "ag", v: [] },
            ao: { t: "ao", v: [] },
            as: { t: "as", v: [] },
            ay: { t: "ay", v: "QUJDYWJjAA==" },
            b: { t: "b", v: false },
            d: { t: "d", v: 43 },
            g: { t: "g", v: "" },
            i: { t: "i", v: 0 },
            n: { t: "n", v: 0 },
            o: { t: "o", v: "/" },
            q: { t: "q", v: 0 },
            s: { t: "s", v: "" },
            t: { t: "t", v: 0 },
            u: { t: "u", v: 0 },
            x: { t: "x", v: 0 },
            y: { t: "y", v: 42 }
        }], "reply");
    });

    QUnit.test("call unimplemented", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "UnimplementedMethod", []);
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.name, "org.freedesktop.DBus.Error.UnknownMethod", "error name");
            // Error message format varies between GDBus and sd-bus
            assert.ok(ex.message.includes("UnimplementedMethod"), "error message mentions method");
            assert.ok(ex.message.includes("com.redhat.Cockpit.DBusTests.Frobber"), "error message mentions interface");
        }
    });

    skipWithPybridge("call bad base64", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call(
                "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TestPrimitiveTypes",
                [10, true, 11, 12, 13, 14, 15, 16, 17, "a string", "/a/path", "asig", "Yooohooo!~ bad base64"]);
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
            assert.equal(ex.message, "Invalid base64 in argument", "error message");
        }
    });

    QUnit.test("call unknown", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "UnknownBlahMethod", [1]);
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.name, "org.freedesktop.DBus.Error.UnknownMethod", "error name");
            assert.equal(ex.message, "Introspection data for method com.redhat.Cockpit.DBusTests.Frobber UnknownBlahMethod not available", "error message");
        }
    });

    QUnit.test("signals", async assert => {
        let received = false;
        const dbus = cockpit.dbus(bus_name, channel_options);
        dbus.subscribe({
            interface: "com.redhat.Cockpit.DBusTests.Frobber",
            path: "/otree/frobber"
        }, (path, iface, signal, args) => {
            if (received)
                return;
            assert.equal(path, "/otree/frobber", "got right path");
            assert.equal(iface, "com.redhat.Cockpit.DBusTests.Frobber", "got right path");
            assert.equal(signal, "TestSignal", "signals: got right path");
            assert.deepEqual(args, [
                43, ["foo", "frobber"], ["/foo", "/foo/bar"],
                { first: [42, 42], second: [43, 43] }], "got right arguments");
            received = true;
        });

        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "RequestSignalEmission", [0]);
        assert.equal(received, true, "signal received");
    });

    QUnit.test("signal unsubscribe", async assert => {
        let received = false;
        const dbus = cockpit.dbus(bus_name, channel_options);

        function on_signal() {
            received = true;
        }

        const subscription = dbus.subscribe({
            interface: "com.redhat.Cockpit.DBusTests.Frobber",
            path: "/otree/frobber"
        }, on_signal);

        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "RequestSignalEmission", [0]);
        assert.equal(received, true, "signal received");

        subscription.remove();
        received = false;
        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "RequestSignalEmission", [0]);
        assert.equal(received, false, "signal not received");
    });

    QUnit.test("with types", assert => {
        const done = assert.async();
        assert.expect(3);

        const dbus = cockpit.dbus(bus_name, channel_options);
        dbus.call("/bork", "borkety.Bork", "Echo",
                  [{ one: "red", two: "blue" }, 55, 66, 32],
                  { type: "a{ss}uit" })
                .done(function(reply, options) {
                    assert.deepEqual(reply, [{ one: "red", two: "blue" }, 55, 66, 32], "round trip");
                    assert.equal(options.type, "a{ss}uit", "got back type");
                })
                .always(function() {
                    assert.equal(this.state(), "resolved", "finished successfully");
                    done();
                });
    });

    skipWithPybridge("empty base64", assert => {
        const done = assert.async();
        assert.expect(3);

        const dbus = cockpit.dbus(bus_name, channel_options);
        dbus.call("/bork", "borkety.Bork", "Echo",
                  [""],
                  { type: "ay" })
                .done(function(reply, options) {
                    assert.deepEqual(reply, [""], "round trip");
                    assert.equal(options.type, "ay", "got back type");
                })
                .always(function() {
                    assert.equal(this.state(), "resolved", "finished successfully");
                    done();
                });
    });

    skipWithPybridge("bad object path", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("invalid/path", "borkety.Bork", "Echo", [1]);
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.problem, "protocol-error", "error name");
            assert.equal(ex.message, "object path is invalid in dbus \"call\": invalid/path", "error message");
        }
    });

    skipWithPybridge("bad interface name", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/path", "!invalid!interface!", "Echo", [1]);
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.problem, "protocol-error", "error name");
            assert.equal(ex.message, "interface name is invalid in dbus \"call\": !invalid!interface!", "error message");
        }
    });

    skipWithPybridge("bad method name", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/path", "borkety.Bork", "!Invalid!Method!", [1]);
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.problem, "protocol-error", "error name");
            assert.equal(ex.message, "member name is invalid in dbus \"call\": !Invalid!Method!", "error message");
        }
    });

    skipWithPybridge("bad flags", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/path", "borkety.Bork", "Method", [1], { flags: 5 });
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.problem, "protocol-error", "error name");
            assert.equal(ex.message, "the \"flags\" field is invalid in dbus call", "error message");
        }
    });

    skipWithPybridge("bad types", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/bork", "borkety.Bork", "Echo", [1], { type: "!!%%" });
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.problem, "protocol-error", "error name");
            assert.equal(ex.message, "the \"type\" signature is not valid in dbus call: !!%%", "error message");
        }
    });

    skipWithPybridge("bad type invalid", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/bork", "borkety.Bork", "Echo", [1], { type: 5 }); // invalid
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.problem, "protocol-error", "error name");
            assert.equal(ex.message, "the \"type\" field is invalid in call", "error message");
        }
    });

    skipWithPybridge("bad dict type", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody",
                            [{ "!!!": "value" }], { type: "a{is}" });
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
            assert.equal(ex.message, "Unexpected key '!!!' in dict entry", "error message");
        }
    });

    skipWithPybridge("bad object path", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody",
                            ["not/a/path"], { type: "o" });
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
            assert.equal(ex.message, "Invalid object path 'not/a/path'", "error message");
        }
    });

    skipWithPybridge("bad signature", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        try {
            await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody", ["bad signature"], { type: "g" });
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
            assert.equal(ex.message, "Invalid signature 'bad signature'", "error message");
        }
    });

    QUnit.test("flags", assert => {
        const done = assert.async();
        assert.expect(3);

        const dbus = cockpit.dbus(bus_name, channel_options);
        dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
                  "HelloWorld", ["test"], { flags: "" })
                .done(function(reply, options) {
                    assert.equal(typeof options.flags, "string", "is string");
                    assert.ok(options.flags.indexOf(">") !== -1 || options.flags.indexOf("<") !== -1, "has byte order");
                })
                .always(function() {
                    assert.equal(this.state(), "resolved", "finished successfully");
                    done();
                });
    });

    QUnit.test("without introspection", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        const reply = await dbus.call("/bork", "borkety.Bork", "Echo");
        assert.deepEqual(reply, [], "round trip");
    });

    QUnit.test("watch path", async assert => {
        const cache = { };

        const dbus = cockpit.dbus(bus_name, channel_options);
        const onnotify = (event, data) => deep_update(cache, data);
        dbus.addEventListener("notify", onnotify);

        await dbus.watch("/otree/frobber");
        assert.equal(typeof cache["/otree/frobber"], "object", "has path");
        assert.deepEqual(cache["/otree/frobber"]["com.redhat.Cockpit.DBusTests.Frobber"],
                         {
                             FinallyNormalName: "There aint no place like home",
                             ReadonlyProperty: "blah",
                             aay: [],
                             ag: [],
                             ao: [],
                             as: [],
                             ay: "QUJDYWJjAA==",
                             b: false,
                             d: 43,
                             g: "",
                             i: 0,
                             n: 0,
                             o: "/",
                             q: 0,
                             s: "",
                             t: 0,
                             u: 0,
                             x: 0,
                             y: 42
                         }, "correct data");
        dbus.removeEventListener("notify", onnotify);
    });

    QUnit.test("watch object manager", async assert => {
        const cache = { };

        const dbus = cockpit.dbus(bus_name, channel_options);
        const onnotify = (event, data) => deep_update(cache, data);
        dbus.addEventListener("notify", onnotify);

        await dbus.watch({ path_namespace: "/otree" });
        assert.deepEqual(cache, {
            "/otree/frobber": {
                "com.redhat.Cockpit.DBusTests.Frobber":
                {
                    FinallyNormalName: "There aint no place like home",
                    ReadonlyProperty: "blah",
                    aay: [],
                    ag: [],
                    ao: [],
                    as: [],
                    ay: "QUJDYWJjAA==",
                    b: false,
                    d: 43,
                    g: "",
                    i: 0,
                    n: 0,
                    o: "/",
                    q: 0,
                    s: "",
                    t: 0,
                    u: 0,
                    x: 0,
                    y: 42
                }
            }
        }, "correct data");
        dbus.removeEventListener("notify", onnotify);
    });

    QUnit.test("watch change", async assert => {
        const cache = { };

        const dbus = cockpit.dbus(bus_name, channel_options);
        const onnotify_cache = (event, data) => deep_update(cache, data);
        dbus.addEventListener("notify", onnotify_cache);

        let onnotify_test_called = false;

        const onnotify_test = (event, data) => {
            assert.equal(typeof cache["/otree/frobber"], "object", "has path");
            assert.deepEqual(cache, {
                "/otree/frobber": {
                    "com.redhat.Cockpit.DBusTests.Frobber": {
                        FinallyNormalName: "There aint no place like home",
                        ReadonlyProperty: "blah",
                        aay: [],
                        ag: [],
                        ao: [],
                        as: [],
                        ay: "QUJDYWJjAA==",
                        b: false,
                        d: 43,
                        g: "",
                        i: 0,
                        n: 0,
                        o: "/",
                        q: 0,
                        s: "",
                        t: 0,
                        u: 0,
                        x: 0,
                        y: 42
                    }
                }
            }, "correct data");
            dbus.removeEventListener("notify", onnotify_cache);
            dbus.removeEventListener("notify", onnotify_test);
            onnotify_test_called = true;
        };
        dbus.addEventListener("notify", onnotify_test);

        await dbus.watch("/otree/frobber");
        assert.ok(onnotify_test_called, "onnotify_test was called");
    });

    QUnit.test("watch barrier", async assert => {
        const cache = { };

        const dbus = cockpit.dbus(bus_name, channel_options);
        const onnotify = (event, data) => deep_update(cache, data);
        dbus.addEventListener("notify", onnotify);

        dbus.watch({ path_namespace: "/otree" });

        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"]);
        assert.deepEqual(cache["/otree/frobber"]["com.redhat.Cockpit.DBusTests.Frobber"],
                         {
                             FinallyNormalName: "There aint no place like home",
                             ReadonlyProperty: "blah",
                             aay: [],
                             ag: [],
                             ao: [],
                             as: [],
                             ay: "QUJDYWJjAA==",
                             b: false,
                             d: 43,
                             g: "",
                             i: 0,
                             n: 0,
                             o: "/",
                             q: 0,
                             s: "",
                             t: 0,
                             u: 0,
                             x: 0,
                             y: 42
                         }, "correct data");
        dbus.removeEventListener("notify", onnotify);
    });

    QUnit.test("watch interfaces", async assert => {
        const cache = { };

        const dbus = cockpit.dbus(bus_name, channel_options);
        const onnotify = (event, data) => deep_update(cache, data);
        dbus.addEventListener("notify", onnotify);

        await dbus.watch({ path_namespace: "/otree" });
        assert.deepEqual(cache, {
            "/otree/frobber": {
                "com.redhat.Cockpit.DBusTests.Frobber":
                {
                    FinallyNormalName: "There aint no place like home",
                    ReadonlyProperty: "blah",
                    aay: [],
                    ag: [],
                    ao: [],
                    as: [],
                    ay: "QUJDYWJjAA==",
                    b: false,
                    d: 43,
                    g: "",
                    i: 0,
                    n: 0,
                    o: "/",
                    q: 0,
                    s: "",
                    t: 0,
                    u: 0,
                    x: 0,
                    y: 42
                }
            }
        }, "correct data");

        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "AddAlpha", []);
        assert.deepEqual(cache, {
            "/otree/frobber": {
                "com.redhat.Cockpit.DBusTests.Frobber":
            {
                FinallyNormalName: "There aint no place like home",
                ReadonlyProperty: "blah",
                aay: [],
                ag: [],
                ao: [],
                as: [],
                ay: "QUJDYWJjAA==",
                b: false,
                d: 43,
                g: "",
                i: 0,
                n: 0,
                o: "/",
                q: 0,
                s: "",
                t: 0,
                u: 0,
                x: 0,
                y: 42
            },
                "com.redhat.Cockpit.DBusTests.Alpha": {}
            }
        }, "correct data");

        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "RemoveAlpha", []);
        assert.deepEqual(cache, {
            "/otree/frobber": {
                "com.redhat.Cockpit.DBusTests.Frobber": {
                    FinallyNormalName: "There aint no place like home",
                    ReadonlyProperty: "blah",
                    aay: [],
                    ag: [],
                    ao: [],
                    as: [],
                    ay: "QUJDYWJjAA==",
                    b: false,
                    d: 43,
                    g: "",
                    i: 0,
                    n: 0,
                    o: "/",
                    q: 0,
                    s: "",
                    t: 0,
                    u: 0,
                    x: 0,
                    y: 42
                },
                "com.redhat.Cockpit.DBusTests.Alpha": null
            }
        }, "correct data");
        dbus.removeEventListener("notify", onnotify);
    });

    skipWithPybridge("path loop", async assert => {
        const name = "yo" + new Date().getTime();
        const cache = { };

        const dbus = cockpit.dbus(bus_name, channel_options);
        const onnotify = (event, data) => Object.assign(cache, data);
        dbus.addEventListener("notify", onnotify);

        await dbus.watch({ path_namespace: "/cliques/" + name });
        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "CreateClique", [name]);
        const expect = { };
        /* The same way mock-service.c calculates the paths */
        for (let i = 0; i < 3; i++) {
            expect["/cliques/" + name + "/" + i] = {
                "com.redhat.Cockpit.DBusTests.Clique": {
                    Friend: "/cliques/" + name + "/" + (i + 1) % 3
                }
            };
        }
        assert.deepEqual(cache, expect, "got all data before method reply");
        dbus.removeEventListener("notify", onnotify);
    });

    skipWithPybridge("path signal", async assert => {
        const name = "yo" + new Date().getTime();
        const cache = { };

        const dbus = cockpit.dbus(bus_name, channel_options);
        const onnotify = (event, data) => Object.assign(cache, data);
        dbus.addEventListener("notify", onnotify);

        await dbus.watch({ path: "/hidden/" + name });
        assert.deepEqual(cache, { }, "no data yet");

        dbus.subscribe({ path: "/hidden/" + name }, function(path, iface, args) {
            assert.equal(typeof cache[path], "object", "have object");
            assert.deepEqual(cache[path], {
                "com.redhat.Cockpit.DBusTests.Hidden": { Name: name }
            }, "got data before signal");
            dbus.removeEventListener("notify", onnotify);
        });
        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "EmitHidden", [name]);
    });

    QUnit.test("proxy", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");
        await proxy.wait();
        assert.strictEqual(proxy.valid, true, "proxy: is valid");
        assert.deepEqual(proxy.data, {
            FinallyNormalName: "There aint no place like home",
            ReadonlyProperty: "blah",
            aay: [],
            ag: [],
            ao: [],
            as: [],
            ay: "QUJDYWJjAA==",
            b: false,
            d: 43,
            g: "",
            i: 0,
            n: 0,
            o: "/",
            q: 0,
            s: "",
            t: 0,
            u: 0,
            x: 0,
            y: 42
        }, "correct data");

        assert.strictEqual(proxy.FinallyNormalName, "There aint no place like home", "property value");
        assert.strictEqual(proxy.ReadonlyProperty, "blah", "another property value");

        assert.equal(typeof proxy.HelloWorld, "function", "has function defined");
        const message = await proxy.HelloWorld("From a proxy");
        assert.equal(message, "Word! You said `From a proxy'. I'm Skeleton, btw!", "method args");
    });

    QUnit.test("proxy call", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");

        /* No wait */
        const args = await proxy.call("HelloWorld", ["From a proxy"]);
        assert.equal(args[0], "Word! You said `From a proxy'. I'm Skeleton, btw!", "method args");
    });

    QUnit.test("proxy call with timeout", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);
        const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");

        try {
            await proxy.call('NeverReturn', [], { timeout: 10 });
            assert.ok(false, "should not be reached");
        } catch (ex) {
            assert.ok(["org.freedesktop.DBus.Error.Timeout",
                "org.freedesktop.DBus.Error.NoReply"].indexOf(ex.name) >= 0);
        }
    });

    QUnit.test("proxy signal", async assert => {
        let received = false;

        const dbus = cockpit.dbus(bus_name, channel_options);
        const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");

        const onsignal = (event, name, args) => {
            assert.equal(name, "TestSignal", "signals: got right name");
            assert.deepEqual(args, [
                43, ["foo", "frobber"], ["/foo", "/foo/bar"],
                { first: [42, 42], second: [43, 43] }], "got right arguments");
            received = true;
        };
        proxy.addEventListener("signal", onsignal);

        await proxy.call("RequestSignalEmission", [0]);
        assert.equal(received, true, "signal received");
        proxy.removeEventListener("signal", onsignal);
    });

    QUnit.test("proxy explicit notify", async assert => {
        const done = assert.async();
        assert.expect(1);

        const dbus = cockpit.dbus(bus_name, channel_options);
        const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");

        await proxy.wait();
        const onchanged = () => {
            assert.equal(proxy.FinallyNormalName, "externally injected");
            proxy.removeEventListener("changed", onchanged);
            done();
        };
        proxy.addEventListener("changed", onchanged);

        dbus.notify({
            "/otree/frobber": {
                "com.redhat.Cockpit.DBusTests.Frobber": {
                    FinallyNormalName: "externally injected"
                }
            }
        });
    });

    QUnit.test("proxies", async assert => {
        const dbus = cockpit.dbus(bus_name, channel_options);

        /* Just some cleanup */
        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "DeleteAllObjects", []);
        const proxies = dbus.proxies("com.redhat.Cockpit.DBusTests.Frobber", "/otree");
        await proxies.wait();

        let added;
        proxies.addEventListener("added", function(event, proxy) {
            added = proxy;
            assert.strictEqual(added.valid, true, "added objects valid");
        });

        let changed;
        proxies.addEventListener("changed", function(event, proxy) {
            changed = proxy;
        });

        let removed;
        proxies.addEventListener("removed", function(event, proxy) {
            removed = proxy;
        });

        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "CreateObject", ["/otree/other"]);
        assert.equal(typeof added, "object", "got added object");
        assert.equal(typeof changed, "object", "no changed object yet");
        assert.equal(typeof removed, "undefined", "no removed object yet");
        assert.equal(added.path, "/otree/other", "added object correct");
        assert.strictEqual(added, changed, "added fires changed");

        changed = null;
        await dbus.call(added.path, added.iface, "RequestPropertyMods", []);
        assert.strictEqual(changed, added, "change fired");

        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "DeleteObject", ["/otree/other"]);
        assert.strictEqual(removed, added, "removed fired");
        assert.strictEqual(removed.valid, false, "removed is invalid");
        dbus.close();
    });
}

export function dbus_track_tests(channel_options, bus_name) {
    QUnit.test("track name", async assert => {
        const name = "yo.x" + new Date().getTime();

        const dbus = cockpit.dbus(bus_name, channel_options);
        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ClaimOtherName", [name]);

        const other = cockpit.dbus(name, {
            bus: channel_options.bus,
            address: channel_options.address,
            track: true
        });
        let gone = false;
        other.addEventListener("close", (_event, data) => {
            assert.strictEqual(data.problem, undefined, "no problem");
            gone = true;
        });
        await other.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["test"]);
        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ReleaseOtherName", [name]);
        assert.strictEqual(gone, true, "close handler was called");
    });

    QUnit.test("no track name", async assert => {
        const name = "yo.y" + new Date().getTime();

        const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ClaimOtherName", [name]);

        const other = cockpit.dbus(name, channel_options);
        let gone = false;
        other.addEventListener("close", () => { gone = true });

        await other.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["test"]);
        await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ReleaseOtherName", [name]);
        try {
            await other.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["test"]);
            assert.ok(false, "call after release should fail");
        } catch (ex) {
            assert.equal(ex.name, "org.freedesktop.DBus.Error.ServiceUnknown");
        }
        assert.equal(gone, false, "is not gone");
    });

    skipWithPybridge("receive readable fd", async assert => {
        const done = assert.async();
        assert.expect(3);

        const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
        const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
                                     "MakeTestFd", ["readable"]);
        assert.equal(typeof (fd.internal), 'string');
        assert.equal(fd.payload, 'stream');

        const channel = cockpit.channel(fd);

        channel.onmessage = (_event, data) => {
            assert.equal(data, 'Hello, fd');
            channel.close();
            done();
        };
    });

    skipWithPybridge("receive readable fd and ensure opening more than once fails", async assert => {
        const done = assert.async();
        assert.expect(6);

        const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
        const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
                                     "MakeTestFd", ["readable"]);
        assert.equal(typeof (fd.internal), 'string');
        assert.equal(fd.payload, 'stream');

        const channel1 = cockpit.channel(fd);
        assert.ok(channel1);
        const channel2 = cockpit.channel(fd);

        channel2.onclose = (_event, options) => {
            assert.equal(options.channel, channel2.id);
            assert.equal(options.command, 'close');
            assert.equal(options.problem, 'not-found');
            done();
        };
    });

    skipWithPybridge("receive readable fd and ensure writing fails", async assert => {
        const done = assert.async();
        assert.expect(5);

        const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
        const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
                                     "MakeTestFd", ["readable"]);
        assert.equal(typeof (fd.internal), 'string');
        assert.equal(fd.payload, 'stream');

        const channel = cockpit.channel(fd);
        channel.send('Hello, fd');

        channel.onclose = (_event, options) => {
            assert.equal(options.channel, channel.id);
            assert.equal(options.command, 'close');
            assert.equal(options.problem, 'protocol-error');
            done();
        };
    });

    skipWithPybridge("receive writable fd", async assert => {
        const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
        const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
                                     "MakeTestFd", ["writable"]);
        assert.equal(typeof (fd.internal), 'string');
        assert.equal(fd.payload, 'stream');

        const channel = cockpit.channel(fd);
        channel.send('Hello, fd');
        channel.close();
    });
}
