#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%require-types
%enable-all-warnings
%new-style
%strict-args
%allow-injection

%requires ../../../../qlib/Util.qm
%requires ../../../../qlib/Logger.qm
%requires ../../../../qlib/DataProvider
%requires ../../../../qlib/FsUtil.qm
%requires ../../../../qlib/QUnit.qm
%requires ../../../../qlib/ConnectionProvider

%include ../../qore/classes/FtpServer.qc

%exec-class ConnectionProviderTest

const CertPath = normalize_dir(get_script_dir() + DirSep + "mycert.pem");
const KeyPath = normalize_dir(get_script_dir() + DirSep + "mykey.pem");
const KeyPassword = "password";

public class ConnectionProviderTest inherits QUnit::Test {
    public {
        const Tags = {
            "a": 1,
            "b": "two",
        };
    }

    constructor() : Test("ConnectionProvider Test", "1.0") {
        addTestCase("cache test", \cacheTest());
        addTestCase("ftp", \ftpConnectionTest());
        addTestCase("http", \httpConnectionTest());
        addTestCase("fs", \fsConnectionTest());
        addTestCase("invalid", \invalidConnectionTest());
        addTestCase("custom parseUrl", \customParseUrlTest());
        addTestCase("custom option validation", \customValidateOptions());

        # Return for compatibility with a test harness that checks the return value
        set_return_value(main());
    }

    cacheTest() {
        assertGt(0, ConnectionSchemeCache::size());
    }

    ftpConnectionTest() {
        FtpServer ftp(0, m_options.verbose);
        on_exit ftp.shutdown();

        int ftp_port = ftp.getPort();

        string url = "ftp://user:pass@localhost:" + ftp_port + "/";
        FtpConnection fc("name", "desc", url);
        assertEq(True, fc.enabled, "connection is enabled by default");
        assertEq(True, fc.monitor, "connection is monitored by default");
        assertEq(False, fc.locked, "connection is unlocked by default");
        assertEq(True, fc.children_can_support_apis);
        assertEq(False, fc.children_can_support_transactions);
        hash<ConnectionInfo> info = fc.getInfo();
        assertNothing(info.features);

        AbstractDataProvider prov = fc.getDataProvider();
        assertEq("ftp", prov.getName());

        # issue #3696: test connection serialization
        FtpConnection conn2 = Serializable::deserialize(fc.serialize());
        assertEq(fc.url, conn2.url);
    }

    httpConnectionTest() {
        TestSslServer server();
        on_exit
            server.stop();
        # use IPv4 localhost to work around potential issues with localhost -> ::1 and IPv6 -> IPv4 mappings
        # (ex: Windows)
        string url = "https://127.0.0.1:" + server.getPort();
        # use += here to ensure that "opts" stays "hash<auto>" and not "hash<string, string>"
        hash<auto> opts += {
            "ssl_cert_path": CertPath,
            "ssl_key_path": KeyPath,
            "ssl_key_password": KeyPassword,
        };
        hash<auto> attributes = {
            "monitor": True,
            "tags": Tags,
        };
        HttpConnection hc("name", "desc", url, attributes, opts);
        assertEq(True, hc.enabled, "connection is enabled by default");
        assertEq(False, hc.locked, "connection is unlocked by default");
        assertEq(True, hc.children_can_support_apis);
        assertEq(False, hc.children_can_support_transactions);

        hash<ConnectionSchemeInfo> scheme_info = hc.getConnectionSchemeInfo();
        assertFalse(scheme_info.options.firstValue().freeform);

        # issue #3696: test connection serialization
        HttpConnection conn2 = Serializable::deserialize(hc.serialize());
        assertEq(hc.url, conn2.url);

        assertEq(Tags, hc.getTags());
        map assertEq($1.value, hc.getTag($1.key)), Tags.pairIterator();
        assertNothing(hc.getTag("invalid"));
        assertThrows("TAG-ERROR", \hc.getTagEx(), "invalid");

        assertTrue(hc.supportsPollingApi());
        {
            SocketPollOperation spop = hc.startPollConnect();
            assertEq("connect", spop.getGoal());
            assertEq("connecting", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("connected", spop.getState());
            assertTrue(spop.goalReached());
            assertNothing(spop.getOutput());
            assertEq("X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT", server.wait());
        }

        HTTPClient client = hc.get();
        assertEq("X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT", server.wait());

        # turn on certificate verification in the server
        server.acceptAllCertificates(False);
        assertEq(False, server.getAcceptAllCertificates());
        client.disconnect();
        # set client to verify server certificate
        client.setSslVerifyMode(SSL_VERIFY_PEER);
        # verify that exceptions are raised in the server & client when connecting
        assertThrows("SOCKET-SSL-ERROR", \client.connect());
        assertEq("SOCKET-SSL-ERROR", server.waitException().err);

        # turn off certificate verification in the server
        server.acceptAllCertificates(True);
        # verify server certificate in the client
        opts.ssl_verify_cert = True;
        hc = new HttpConnection("name", "desc", url, attributes, opts);
        # verify that exceptions are raised in the server & client when connecting
        assertThrows("SOCKET-SSL-ERROR", \hc.get());
        assertEq("SOCKET-SSL-ERROR", server.waitException().err);
    }

    fsConnectionTest() {
        string url = Util::tmp_location();
        hash<auto> opts = (
            "readonly": False,
        );
        hash<auto> attributes = {
            "monitor": True,
        };

        FilesystemConnection fs("name", "desc", url, attributes, opts);
        assertEq(True, fs.enabled, "connection is enabled by default");
        assertEq(False, fs.locked, "connection is unlocked by default");
        assertEq(False, fs.opts.readonly, "readonly enabled by opts");

        # issue #3696: test connection serialization
        FilesystemConnection conn2 = Serializable::deserialize(fs.serialize());
        assertEq(fs.url, conn2.url);

        Dir d = fs.get();
        assertEq(Util::tmp_location(), d.path(), "Path equality");
    }

    invalidConnectionTest() {
        InvalidConnection ic1("name", "desc", "url");
        assertEq("unknown error", ic1.error);

        hash<auto> attributes = {
            "error": "lorem ipsum",
        };
        InvalidConnection ic2("name", "desc", "url", attributes);
        assertEq(attributes.error, ic2.error);

        {
            string url = "https://example.com";
            InvalidConnection c("x", "x", url);
            assertEq(url, c.getInfo().url);
        }
    }

    customParseUrlTest() {
        # and now the custom parsing
        MyConnection db();
        assertEq("pgsql", db.urlh.type);
        assertEq("user", db.urlh.user);
        assertEq("pass", db.urlh.pass);
        assertEq("db", db.urlh.db);
        assertEq("host", db.urlh.host);
        assertEq(1234, db.urlh.port);

        assertEq(True, db.locked, "test locked connection");

        # safe url
        assertEq("pgsql:user@db%host:1234", db.safe_url);
    }

    private customValidateOptions() {
        hash<auto> opts = {"foo": "bar"};

        try {
            HttpConnection h("foo", "bar", "http://localhost", {}, opts);
            assertEq(False, exists h);
        } catch (hash<ExceptionInfo> ex) {
            assertEq("CONNECTION-ERROR", ex.err);
            assertEq(True, ex.desc =~ /unknown option/);
        }

        IgnoreOptionsConnection h("foo", "bar", "http://localhost", {}, opts);
        assertEq(opts, h.opts);
    }
}

class IgnoreOptionsConnection inherits HttpConnection {
    constructor(string name, string desc, string url, hash<auto> attrs, hash<auto> opts)
        : HttpConnection(name, desc, url, attrs, opts) {
    }

    hash<auto> validateOptions(hash<auto> options) {
        return options;
    }
}

class MyConnection inherits AbstractConnection {
    public {
        # let's simulate a DB connection (without DatasourcePool options for a simplicity)
        const URL = "sql://pgsql:user/pass@db%host:1234";
    }

    constructor() : AbstractConnection("name", "desc", URL, {"locked": True}) {
    }

    private hash<auto> parseUrl(string url) {
        url = replace(url, "sql://", "");
        return parse_datasource(url);
    }

    private string getSafeUrl(hash<auto> urlh) {
        string url = sprintf("%s:%s@%s", urlh.type, urlh.user, urlh.db);
        if (urlh.host.val()) {
            url += "%" + urlh.host;
        }
        if (urlh.port.val()) {
            url += ":" + urlh.port;
        }
        return url;
    }

    # just to implement abstract methods
    string getType() {
        return "sql";
    }

    object getImpl(bool _ = True, *hash<auto> __) {
        return new Dir(); # just a silence the missing abtsract method. No use for Dir at all
    }
}

class TestSslServer {
    private:internal {
        Socket sock();
        Queue q();
        bool exit;
    }

    constructor() {
        sock.setCertificate(ReadOnlyFile::readTextFile(CertPath));
        sock.setPrivateKey(ReadOnlyFile::readTextFile(KeyPath), KeyPassword);
        sock.setSslVerifyMode(SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
        sock.acceptAllCertificates(True);

        if (sock.bind("localhost:0"))
            throw "BIND-ERROR", strerror();

        if (sock.listen())
            throw "LISTEN-ERROR", strerror();

        background listen();
    }

    stop() {
        exit = True;
    }

    acceptAllCertificates(bool accept_all = True) {
        sock.acceptAllCertificates(accept_all);
    }

    bool getAcceptAllCertificates() {
        return sock.getAcceptAllCertificates();
    }

    private:internal listen() {
        while (!exit) {
            try {
                *Socket a = sock.acceptSSL(100ms);
                if (a) {
                    #printf("got %y\n", a.verifyPeerCertificate());
                    q.push(a.verifyPeerCertificate());
                }
            } catch (hash<ExceptionInfo> ex) {
                #printf("got ex: %s: %s\n", ex.err, ex.desc);
                q.push(ex);
            }
        }
    }

    *string wait() {
        return q.get();
    }

    *hash<auto> waitException() {
        return q.get();
    }

    int queueSize() {
        return q.size();
    }

    int getPort() {
        return sock.getSocketInfo().port;
    }
}
