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

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

%requires ../../../../qlib/Util.qm
%requires ../../../../qlib/QUnit.qm
%requires ../../../../qlib/DataProvider
%requires ../../../../qlib/ConnectionProvider
%requires ../../../../qlib/Logger.qm
%requires ../../../../qlib/HttpServerUtil.qm
%requires ../../../../qlib/HttpServer.qm
%requires ../../../../qlib/WebSocketUtil.qm
%requires ../../../../qlib/WebSocketHandler.qm
%requires ../../../../qlib/WebSocketClient.qm

%exec-class WebSocketClientTest

class MyWsConnection inherits WebSocketConnection {
    constructor(WebSocketHandler handler) : WebSocketConnection(handler) {
    }

    sendData() {
        map send($1), WebSocketClientTest::Data;
    }

    gotMessage(string msg) {
        send("RESPONSE:" + msg);
        #printf("GOT MSG: %y\n", msg);
    }
}

class MyWsHandler inherits WebSocketHandler {
    public {
        bool debug = False;
    }

    WebSocketConnection getConnectionImpl(hash<auto> cx, hash<auto> hdr, string cid) {
        MyWsConnection conn(self);
        if (hdr."send-data") {
            conn.sendData();
        }
        return conn;
    }

    logError(string fmt) {
        if (debug) {
            vprintf("ERROR " + fmt + "\n", argv);
        }
    }

    logInfo(string fmt) {
        if (debug) {
            vprintf("INFO " + fmt + "\n", argv);
        }
    }

    logDebug(string fmt) {
        if (debug) {
            vprintf("DEBUG " + fmt + "\n", argv);
        }
    }
}

class WebSocketClientTest inherits QUnit::Test {
    public {
        HttpServer mServer;
        MyWsHandler wsHandler();
        int port;
        int responseSize;

        Logger logger;

        const Data = (
            "test1",
            <beeffeed>,
            "test3",
        );
    }

    constructor() : Test("WebSocketClientTest", "1.0") {
        addTestCase("data provider test", \dataProviderTest());
        addTestCase("WebSocketClient tests", \webSocketClientTests());
        addTestCase("connection tests", \connectionTests());

        logger = new Logger("test", LoggerLevel::getLevelInfo());
        if (m_options.verbose > 2) {
            logger.addAppender(new StdoutAppender());
        }
        hash<HttpServerOptionInfo> http_opts = <HttpServerOptionInfo>{
            "logger": logger,
            "debug": True,
        };
        mServer = new HttpServer(http_opts);
        mServer.setHandler("ws-handler", "", NOTHING, wsHandler);
        mServer.setDefaultHandler("ws-handler", wsHandler);
        port = mServer.addListener(<HttpListenerOptionInfo>{"service": 0}).port;

        set_return_value(main());

        mServer.stop();
    }

    dataProviderTest() {
        MyObserver observer();
        WebSocketClientDelayedDataProvider prov({
            "url": "ws://localhost:" + port,
            "data": "none",
        });
        prov.setLogger(logger);
        prov.registerObserver(observer);
        prov.observersReady();

        prov.sendMessage(MESSAGE_WS_PING, "hb");

        observer.wait();
        assertEq(1, observer.l.size());
        assertEq(EVENT_WS_PONG, observer.l[0].id);

        AbstractDataProvider child = prov.getChildProvider("wait-for-message");
        prov.sendMessage(MESSAGE_WS_DATA, "hi");

        auto response = child.doRequest();
        assertEq("RESPONSE:hi", response);

        # negative test
        assertThrows("WEBSOCKET-RECV-TIMEOUT", \child.doRequest(), {"timeout": 1});
    }

    webSocketClientTests() {
        list<auto> l = ();
        Counter c(1);
        code cb = sub (*data msg) {
            #printf("RECEIVED: %y\n", msg);
            if (exists msg) {
                l += msg;
                if (l.size() == responseSize)
                    c.dec();
            }
        };
        responseSize = Data.size();
        code log = code sub (string str) {
            return sub (string fmt) {
                #vprintf("CLIENT " + str + ": " + fmt + "\n", argv);
                remove argv;
            };
        };
        hash<auto> opts = {
            "url": "ws://localhost:" + port,
            "log": log("INFO"),
            "errlog": log("ERROR"),
            "debuglog": log("DEBUG"),
        };
        WebSocketClient wsc(cb, opts);
        wsc.connect({"hdr": {"Send-Data": 1}});
        c.waitForZero();
        assertEq(Data, l);
        c.inc();
        l = ();
        responseSize = 1;
        # issue #2566: send a unidirectional PONG to make sure that the WebSocketHandler can handle it
        # https://tools.ietf.org/html/rfc6455#section-5.5.3: unsolicited PONG messages must be ignored
        wsc.pong("heartbeat");
        wsc.send("REQUEST");
        c.waitForZero();
        assertEq(list("RESPONSE:REQUEST"), l);
        wsc.disconnect();
        delete wsc;
    }

    connectionTests() {
        string url = "ws://localhost:8099";
        WebSocketConnectionObject conn("test", "test", url);
        WebSocketClient client = conn.get(False);
        assertEq(url, client.getUrl());

        conn = new WebSocketConnectionObject("test", "test", url, {"monitor": False});
        client = conn.get(False);
        assertEq(url, client.getUrl());
    }

    private log(string str) {
        if (m_options.verbose > 2)
            vprintf(str + "\n", argv);
    }
}

class MyObserver inherits Observer {
    public {
        list<hash<auto>> l();
    }

    private {
        Mutex m();
        Condition cond();
        int waiting;
    }

    update(string event_id, hash<auto> data_) {
        m.lock();
        on_exit m.unlock();

        l += {
            "id": event_id,
            "data": data_,
        };

        if (waiting) {
            cond.broadcast();
        }
    }

    wait() {
        date start = now_us();

        m.lock();
        on_exit m.unlock();

        while (!l) {
            cond.wait(m, 1s);
            if ((now_us() - start) > 5s) {
                throw "ERROR", "no data received in timeout";
            }
        }
    }
}
