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

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

%requires ../../../../qlib/QUnit.qm
%requires ../../../../qlib/Mime.qm
%requires ../../../../qlib/Logger.qm
%requires ../../../../qlib/HttpServerUtil.qm
%requires ../../../../qlib/DataProvider
%requires ../../../../qlib/ConnectionProvider
%requires ../../../../qlib/RestSchemaValidator.qm
%requires ../../../../qlib/Swagger.qm
%requires ../../../../qlib/RestClient.qm
%requires ../../../../qlib/HttpServer.qm
%requires ../../../../qlib/FsUtil.qm

%try-module yaml >= 0.5
%define NoYaml
%endtry

%try-module xml >= 1.3
%define NoXml
%endtry

%try-module json >= 1.5
%define NoJson
%endtry

%exec-class Main

const VALUE_TEXT = {
    "input": "Lorem Ipsum",
    "output": "Lorem Ipsum",
};
const VALUE_XML = {
    "input": "<foo><bar>1</bar></foo>",
    "output": {"foo": {"bar": "1"}},
};
const VALUE_XMLRPC = {
    "input": "<struct><member><name>foo</name><value><i4>1</i4></value></member></struct>",
    "output": {"foo": 1},
};
const VALUE_YAML = {
    "input": "foo:\n bar: 1\n other: '2017-01-01 00:00:00Z'",
    "output": {"foo": {"bar": 1, "other": 2017-01-01Z}},
};
const VALUE_JSON = {
    "input" : "{\"foo\":{\"bar\":1}}",
    "output" : {"foo" : {"bar" : 1}},
};

class SimpleStringHandler inherits AbstractHttpRequestHandler {
    private {
        *string m_data;
        string m_mime;
    }
    constructor (string mime, *string str) {
        m_mime = mime;
        m_data = str;
    }
    hash<auto> handleRequest(hash<auto> cx, hash<auto> hdr, *data body) {
        if (hdr."do-error") {
            return makeResponse(404, "test error", {"Content-Type": MimeTypeText});
        }
        if (hdr.method == "GET") {
            return makeResponse(200, m_data ?? binary(body), {"Content-Type": m_mime});
        } else if (hdr.method == "PUT") {
            return makeResponse(200, m_data ?? binary(body), {"Content-Type": m_mime});
        } else if (hdr.method == "PATCH") {
            return makeResponse(200, m_data ?? binary(body), {"Content-Type": m_mime});
        } else if (hdr.method == "POST") {
            return makeResponse(200, m_data ?? binary(body), {"Content-Type": m_mime});
        } else if (hdr.method == "DELETE") {
            return makeResponse(404, m_data ?? binary(body), {"Content-Type": m_mime});
        } else if (hdr.method == "OPTIONS") {
            return makeResponse(200, m_data ?? binary(body), {"Content-Type": m_mime});
        }
    }
}

public class Main inherits QUnit::Test {
    private {
        HttpServer m_http;

        int port;

        Logger logger;

        const TestSchema = {
            "swagger": "2.0",
            "info": {
                "title": "title",
                "version": "1.0",
            },
            "basePath": "/",
            "paths": {
                "/api_yaml": {
                    "put": {
                       "produces": (MimeTypeYaml,),
                        "consumes": (MimeTypeYaml,),
                        "parameters": (
                            {
                                "in": "body",
                                "name": "body",
                                "required": True,
                                "schema": {
                                    "$ref": "#/definitions/MyObject",
                                },
                            },
                        ),
                        "responses": (
                            "200": (
                                "description": "",
                                "schema": (
                                    "$ref": "#/definitions/MyObject",
                                ),
                            ),
                        ),
                    },
                },
            },
            "definitions": (
                "MyObject": (
                    "type": "object",
                    "properties": {
                        "foo": {
                            "type": "object",
                            "description": "the foo object",
                            "properties": {
                                "bar": {
                                    "type": "integer",
                                    "description": "the bar int",
                                },
                                "other": {
                                    "type": "string",
                                    "format": "date",
                                    "desc": "the other date",
                                },
                            },
                        },
                    },
                ),
            ),
        };
    }

    constructor() : Test("RestClientTest", "1.0") {
        addTestCase("invalid connection option", \invalidConnectionOptionTest());
        addTestCase("poll test", \pollTest());
        addTestCase("issue 3472", \issue3472());
        addTestCase("connection tests", \connectionTests());
        addTestCase("OPTIONS tests", \methodOptionsTest());
        addTestCase("DELETE tests", \methodDeleteTest());
        addTestCase("GET tests", \methodGetTest());
        addTestCase("PUT tests", \methodPutTest());
        addTestCase("POST tests", \methodPostTest());

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

    setUp() {
        logger = new Logger("test", (m_options.verbose > 3 ? LoggerLevel::getLevelDebug() : LoggerLevel::getLevelInfo()));
        if (m_options.verbose > 2) {
            logger.addAppender(new StdoutAppender());
        }
        hash<HttpServerOptionInfo> http_opts = <HttpServerOptionInfo>{
            "logger": logger,
            "debug": True,
        };
        m_http = new HttpServer(http_opts);
        m_http.setHandler("/api_text", "/api_text", MimeTypeText, new SimpleStringHandler(MimeTypeText, VALUE_TEXT.input));
        m_http.setHandler("/api_xml", "/api_xml", MimeTypeXmlApp, new SimpleStringHandler(MimeTypeXmlApp, VALUE_XML.input));
        m_http.setHandler("/api_rpc", "/api_rpc", MimeTypeXml, new SimpleStringHandler(MimeTypeXml, VALUE_XMLRPC.input));
        m_http.setHandler("/api_json", "/api_json", MimeTypeJson, new SimpleStringHandler(MimeTypeJson, VALUE_JSON.input));
        m_http.setHandler("/api_yaml", "/api_yaml", MimeTypeYaml, new SimpleStringHandler(MimeTypeYaml, VALUE_YAML.input));
        m_http.setHandler("/api_bin", "/api_bin", MimeTypeOctetStream, new SimpleStringHandler(MimeTypeOctetStream));
        m_http.setHandler("/swagger", "/swagger", MimeTypeYaml, new SimpleStringHandler(MimeTypeYaml, Main::getSwaggerSchema()));
        port = m_http.addListener(<HttpListenerOptionInfo>{"service": 0}).port;
    }

    tearDown() {
        delete m_http;
    }

    invalidConnectionOptionTest() {
        assertThrows("CONNECTION-OPTION-ERROR", sub () {
            new RestConnection("test", "test", "rests://127.0.0.1", NOTHING, {"data": "invalid"});
        });
    }

    pollTest() {
        string url = "http://127.0.0.1:" + port;
        RestClient rest({"url": url});

        {
            SocketPollOperation spop = rest.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());
        }

        RestConnection conn("test", "test", url);
        assertTrue(conn.supportsPollingApi());
        {
            SocketPollOperation spop = conn.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());
        }
    }

    issue3472() {
        RestClient rest({"url": "http://127.0.0.1:" + port});
        assertThrows("REST-RESPONSE-ERROR", \rest.get(), ("/api_text", NOTHING, NOTHING, {"Do-Error": True}));
        rest.setErrorPassthru(True);
        hash<auto> msg = rest.get("/api_text", NOTHING, NOTHING, {"Do-Error": True});
        assertEq(404, msg.status_code);
        assertEq("test error", msg.body);
    }

    methodOptionsTest() {
        RestClient rest({"url": "http://127.0.0.1:" + port});
        hash<auto> info;
        rest.doRequest("OPTIONS", "/api_bin", NOTHING, \info);
        assertEq("0", info."response-headers"."content-length");
    }

    methodDeleteTest() {
        RestClient rest({"url": "http://127.0.0.1:" + port});
        hash<auto> info;
        try {
            # issue #3237: must throw an exception due to the 404 Not Found response
            rest.del("/api_text", NOTHING, \info);
            assertTrue(False);
        } catch (hash<ExceptionInfo> ex) {
        }
        assertEq(404, info."response-code");
    }

    methodGetTest() {
        RestClient rest(("url" : "http://127.0.0.1:" + port));
        auto ret;

        ret = rest.get("/api_text");
        testAssertionValue("/api_text", ret.body, VALUE_TEXT.output);

%ifndef NoXml
        ret = rest.get("/api_xml");
        testAssertionValue("/api_xml", ret.body, VALUE_XML.output);

        ret = rest.get("/api_rpc");
        testAssertionValue("/api_rpc", ret.body, VALUE_XMLRPC.output);
%endif

%ifndef NoJson
        ret = rest.get("/api_json");
        testAssertionValue("/api_json", ret.body, VALUE_JSON.output);
%endif

%ifndef NoYaml
        ret = rest.get("/api_yaml");
        testAssertionValue("/api_yaml", ret.body, VALUE_YAML.output);
%endif
    }

    methodPutTest() {
        RestClient rest(("url" : "http://127.0.0.1:" + port));
        auto ret;

        rest.setSerialization("text");
        ret = rest.put("/api_text", VALUE_TEXT.output);
        testAssertionValue("/api_text", ret.body, VALUE_TEXT.output);

        ret = rest.patch("/api_text", VALUE_TEXT.output);
        assertEq(VALUE_TEXT.output, ret.body);

%ifndef NoXml
        rest.setSerialization("rawxml");
        ret = rest.put("/api_xml", VALUE_XML.output);
        testAssertionValue("/api_xml", ret.body, VALUE_XML.output);

        rest.setSerialization("xml");
        ret = rest.put("/api_rpc", VALUE_XMLRPC.output);
        testAssertionValue("/api_rpc", ret.body, VALUE_XMLRPC.output);
%endif

%ifndef NoJson
        rest.setSerialization("json");
        ret = rest.put("/api_json", VALUE_JSON.output);
        testAssertionValue("/api_json", ret.body, VALUE_JSON.output);
%endif

%ifndef NoYaml
        rest.setSerialization("yaml");
        ret = rest.put("/api_yaml", VALUE_YAML.output);
        testAssertionValue("/api_yaml", ret.body, VALUE_YAML.output);
%endif
    }

    methodPostTest() {
        RestClient rest({"url": "http://127.0.0.1:" + port});
        auto ret;

        rest.setSerialization("text");
        hash<auto> info;
        #on_exit printf("info: %N\n", info);
        ret = rest.post("/api_bin", <abcd>, \info);
        #printf("ret: %y\n", ret);
        assertEq(<abcd>, binary(ret.body));
        assertEq(MimeTypeOctetStream, ret."content-type");

        rest.setSerialization("bin");
        #on_exit printf("info: %N\n", info);
        ret = rest.post("/api_bin", <abcd>, \info);
        #printf("ret: %y\n", ret);
        assertEq(<abcd>, binary(ret.body));
        assertEq(MimeTypeOctetStream, ret."content-type");
    }

    connectionTests() {
        string url = "http://127.0.0.1:8099/";
        RestClient client;
        {
            RestConnection conn("test", "test", url);
            client = conn.get(False);
            assertEq(url, client.getURL());

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

            # issue #3321: check default timeouts
            assertEq(45000, client.getTimeout());
            assertEq(45000, client.getConnectTimeout());
        }

        string val = get_random_string();
        hash<auto> hdr = {
            "X-Test": val,
        };
        {
            RestConnection conn("test", "test", url, {"monitor": False}, {"headers": hdr});
            client = conn.get(False);
            assertEq(hdr, client.getDefaultHeaders(){"X-Test",});
            assertEq(url, client.getURL());
            assertEq(False, conn.monitor);
            assertEq(True, conn.enabled);

            assertThrows("OAUTH2-ERROR", \conn.getAuthorizationCodeRequest());
        }

        {
            RestConnection conn("test", "test", url, {"monitor": False}, {"headers": "X-Test=" + val});
            client = conn.get(False);
            assertEq(hdr, client.getDefaultHeaders(){"X-Test",});
        }

        {
            RestConnection conn("test", "test", url, {"monitor": False}, {"headers": "abc=2, X-Test=" + val});
            client = conn.get(False);
            assertEq({"abc": "2"} + hdr, client.getDefaultHeaders(){"abc", "X-Test",});
        }

        assertThrows("REST-CONNECTION-ERROR", sub () {
            new RestConnection("test", "test", url, {"monitor": False}, {"headers": "1"});
        });

        url = "http://127.0.0.1:" + port;
        {
            RestConnection conn("test", "test", url);
            assertTrue(conn.supportsPollingApi());
            {
                SocketPollOperation spop = conn.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());
            }
        }

        {
            RestConnection rc("test", "test", url, {}, {
                "data": "text",
                "ping_method": "PUT",
                "ping_path": "/api_text",
                "ping_body": VALUE_TEXT.input,
            });

            TestPollingConnectionMonitor monitor(logger);
            on_exit delete monitor;

            assertTrue(rc.getInfo().up);

            # first test a standard ping
            hash<PingInfo> ping = rc.ping(True);
            assertEq("test", ping.name);
            assertTrue(ping.ok);
            assertEq("OK", ping.info);
            assertTrue(rc.getInfo().up);

            monitor.add(rc);

            hash<auto> info = monitor.waitForResult();
            assertEq(info.name, "test");
            on_error if (info.ex) {
                printf("%s\n", get_exception_string(info.ex));
            }
            assertEq("OK", info.result);
            assertTrue(rc.getInfo().up);
        }

%ifndef NoYaml
        {
            RestConnection rc("test", "test", url, {}, {
                "data": "yaml",
                "ping_method": "PUT",
                "ping_path": "/api_yaml",
                "ping_body": VALUE_YAML.output,
                "swagger": url + "/swagger",
            });

            # first test a standard ping
            hash<PingInfo> ping = rc.ping(True);
            assertEq("test", ping.name);
            assertTrue(ping.ok);
            assertEq("OK", ping.info);

            TestPollingConnectionMonitor monitor(logger);
            on_exit delete monitor;

            monitor.add(rc);

            hash<auto> info = monitor.waitForResult();
            assertEq(info.name, "test");
            on_error if (info.ex) {
                printf("%s\n", get_exception_string(info.ex));
            }
            assertEq("OK", info.result);
        }

        {
            TmpFile tmp();
            tmp.file.write(getSwaggerSchema());
            tmp.file.close();

            RestConnection rc("test", "test", url, {}, {
                "data": "yaml",
                "ping_method": "PUT",
                "ping_path": "/api_yaml",
                "ping_body": VALUE_YAML.output,
                "swagger": "file://" + tmp.path,
                "encode_chars": "+",
            });

            hash<ConnectionInfo> cinfo = rc.getInfo();
            assertTrue(cinfo.features{AbstractConnection::CF_LOGGER});

            # first test a standard ping
            hash<PingInfo> ping = rc.ping(True);
            assertEq("test", ping.name);
            assertTrue(ping.ok);
            assertEq("OK", ping.info);

            TestPollingConnectionMonitor monitor(logger);
            on_exit delete monitor;

            monitor.add(rc);

            hash<auto> info = monitor.waitForResult();
            assertEq(info.name, "test");
            on_error if (info.ex) {
                printf("%s\n", get_exception_string(info.ex));
            }
            assertEq("OK", info.result);
        }
%endif

        {
            string auth_uri = "https://example.com/auth";
            string token_uri = "https://example.com/token";
            RestConnection rc("test", "test", url, {}, {
                "data": "yaml",
                "oauth2_grant_type": "authorization_code",
                "oauth2_auth_url": auth_uri,
                "oauth2_token_url": token_uri,
                "oauth2_client_id": get_random_string(),
                "oauth2_client_secret": get_random_string(),
                "oauth2_redirect_url": "https://other.app.com",
            });

            string uri = rc.getAuthorizationCodeRequest();
            assertTrue(uri.equalPartialPath(auth_uri));
            hash<ConnectionInfo> cinfo = rc.getInfo();
            assertTrue(cinfo.features{AbstractConnection::CF_UPDATE_OPTIONS});
            assertTrue(cinfo.features{RestConnection::RCF_OAUTH2_AUTH_CODE});
        }

        {
            string auth_uri = "https://example.com/auth";
            string token_uri = "https://example.com/token";
            RestConnection rc("test", "test", url, {}, {
                "data": "yaml",
                "oauth2_grant_type": "authorization_code",
                "oauth2_auth_url": auth_uri,
                "oauth2_token_url": token_uri,
                "oauth2_client_id": get_random_string(),
                "oauth2_client_secret": get_random_string(),
                "oauth2_redirect_url": "https://other.app.com",
                "oauth2_auth_args": {
                    "access_type": "offline",
                    "include_granted_scopes": True,
                },
                "oauth2_scopes": (
                    "scope1",
                    "scope2",
                ),
            });

            assertEq("yaml", rc.getRealOptions()."data");
            string uri = rc.getAuthorizationCodeRequest();
            assertTrue(uri.equalPartialPath(auth_uri));
            assertRegex("&access_type=offline", uri);
            assertRegex("&include_granted_scopes=true", uri);
            assertRegex("scope1%20scope2", uri);
            hash<ConnectionInfo> cinfo = rc.getInfo();
            assertTrue(cinfo.features{AbstractConnection::CF_UPDATE_OPTIONS});
            assertTrue(cinfo.features{RestConnection::RCF_OAUTH2_AUTH_CODE});
        }
    }

    static *string getSwaggerSchema() {
%ifndef NoYaml
        return make_yaml(TestSchema);
%endif
    }
}

class TestPollingConnectionMonitor inherits PollingConnectionMonitor {
    private {
        Queue msgq();
    }

    constructor(*Logger logger) : PollingConnectionMonitor(logger) {
        ping_timeout = 5s;
    }

    hash<auto> waitForResult() {
        return msgq.get();
    }

    private handlePingSuccess(string name, date delta, *bool oldok) {
        PollingConnectionMonitor::handlePingSuccess(name, delta, oldok);
        msgq.push({"name": name, "result": "OK"});
    }

    private handlePingFailed(string name, date delta, hash<ExceptionInfo> ex) {
        PollingConnectionMonitor::handlePingFailed(name, delta, ex);
        msgq.push({"name": name, "result": "error", "ex": ex});
    }

    private handlePingTimeout(string name, date delta) {
        PollingConnectionMonitor::handlePingTimeout(name, delta);
        msgq.push({"name": name, "result": "timeout"});
    }
}
