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

%requires ../../../../qlib/Util.qm
%requires ../../../../qlib/QUnit.qm
%requires ../../../../qlib/Mime.qm
%requires ../../../../qlib/MapperUtil.qm
%requires ../../../../qlib/Logger.qm
%requires ../../../../qlib/DataProvider
%requires ../../../../qlib/ConnectionProvider
%requires ../../../../qlib/RestClient.qm
%requires ../../../../qlib/ServiceNowRestClient.qm
%requires ../../../../qlib/ServiceNowRestDataProvider

%try-module json
%define NoJson
%endtry

%requires qore >= 0.9.10

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

%exec-class ServiceNowTest

class ServiceNowTest inherits QUnit::Test {
    public {}

    private {
        ServiceNowRestClient rc;
        Logger logger;

        # ServiceNow connection options
        hash<auto> snopts;

        # command-line options
        const MyOpts = Opts + {
            "username":       "U,username=s",
            "password":       "p,password=s",
            "client_id":      "i,client_id=s",
            "client_secret":  "s,client_secret=s",
            "timeout":        "t,timeout=i",
            "url":            "u,url=s",
        };

        const OptionColumn = 22;

        const AcctNo1 = "DEL_TEST_1";
        const AcctName1 = "My Test Account 1";

        const AcctNo2 = "DEL_TEST_2";
    }

    constructor() : Test("ServiceNowTest", "1.0", \ARGV, MyOpts) {
        addTestCase("ServiceNow DataProvider test", \ServiceNowDataProviderTests());
        addTestCase("ServiceNow REST", \ServiceNowRestTests());
        addTestCase("connection tests", \connectionTest());

        logger = new Logger("test",
            (m_options.verbose > 3 ? LoggerLevel::getLevelDebug() : LoggerLevel::getLevelInfo()));
        if (m_options.verbose > 2) {
            logger.addAppender(new StdoutAppender());
        }

        try {
            setupConnection();
        } catch (hash<ExceptionInfo> ex) {
            if (m_options.verbose > 2) {
                printf("%s\n", get_exception_string(ex));
            } else if (m_options.verbose) {
                printf("%s: %s\n", ex.err, ex.desc);
            }
        }

        set_return_value(main());
    }

    private usageIntern() {
        TestReporter::usageIntern(OptionColumn);
        printOption("-i,--client_id=ARG", "set ServiceNow consumer key", OptionColumn);
        printOption("-p,--password=ARG", "set ServiceNow pass", OptionColumn);
        printOption("-s,--client_secret=ARG", "set ServiceNow consumer secret", OptionColumn);
        printOption("-t,--timeout=ARG", "set ServiceNow timeout in seconds", OptionColumn);
        printOption("-U,--username=ARG", "set ServiceNow user", OptionColumn);
        printOption("-u,--url=ARG", "URL for the ServiceNow instance", OptionColumn);
    }

    setupConnection() {
        snopts.url = m_options.url ?? ENV.SERVICENOW_URL;
        if (!snopts.url)
            error("missing --url option or SERVICENOW_URL environment variable");
        if (m_options.client_id || ENV.SERVICENOW_CLIENT_ID
            && (m_options.client_secret || ENV.SERVICENOW_CLIENT_SECRET)) {
            snopts.client_id = m_options.client_id ?? ENV.SERVICENOW_CLIENT_ID;
            snopts.client_secret = m_options.client_secret ?? ENV.SERVICENOW_CLIENT_SECRET;
        }
        snopts.username = m_options.username ?? ENV.SERVICENOW_USERNAME;
        if (!snopts.username) {
            error("missing --username option or SERVICENOW_USERNAME environment variable");
        }
        snopts.password = m_options.password ?? ENV.SERVICENOW_PASSWORD;
        if (!snopts.password) {
            error("missing --password option or SERVICENOW_PASSWORD environment variable");
        }
        if (snopts."timeout") {
            snopts.connect_timeout = snopts."timeout";
        }
        snopts.oauth2_grant_type = "password";

        rc = new ServiceNowRestClient(snopts + {"logger": logger});
    }

    private error(string fmt) {
        throw "OPTION-ERROR", sprintf("%s: ERROR: %s", get_script_name(), vsprintf(fmt, argv));
    }

    ServiceNowDataProviderTests() {
        if (!rc) testSkip("no ServiceNow connection");

        assertEq(snopts.username, rc.getUsername());

        AbstractDataProvider dp = new ServiceNowRestDataProvider(rc);
        dp = dp.getChildProvider("tables");

        assertFalse(dp.requiresTransactionManagement());
        {
            list<string> children = dp.getChildProviderNames();
            assertEq(Type::List, children.type());
            hash<string, bool> child_map = map {$1: True}, children;
            assertTrue(child_map.incident);
        }

        list<hash<auto>> recs;
        {
            AbstractDataProvider incident = dp.getChildProvider("incident");

            string desc = get_random_string();
            string id;
            {
                # create new incident
                hash<auto> rec = {
                    "short_description": desc,
                };
                id = incident.createRecord(rec).sys_id;
            }
            on_exit {
                # delete the record
                assertEq(1, incident.deleteRecords({"sys_id": id}));
            }

            # search the record
            recs = map $1, incident.searchRecords({"short_description": desc}, {"limit": 1});
            assertEq(1, recs.size());
            assertEq(id, recs[0].sys_id);

            # update the record
            assertEq(1, incident.updateRecords({"short_description": "deleteme-now"}, {"sys_id": id}));
        }
    }

    ServiceNowRestTests() {
        if (!rc) testSkip("no ServiceNow connection");

        assertEq("now", rc.getApi());

        if (rc.usingOAuth2()) {
            assertEq(Type::String, rc.getToken().type());
        }

        {
            hash<auto> info;
            hash<auto> h = rc.get("table/incident?sysparm_limit=1", NOTHING, \info);
            on_error printf("%N\n", h);
            assertEq(Type::Hash, h.body.response.result.type());
        }

%ifdef NoJson
        testSkip("no json module present");
%endif

        hash<auto> info = parse_url(rc.getURL());
        string url = info.protocol + "://" + info.host;
        if (info.port) {
            url += ":" + info.port;
        }

        {
            # setup valid connection and test polling
            ServiceNowRestConnection snrc("test", "test", url, {"monitor": True}, {
                "username": snopts.username,
                "password": snopts.password,
            });

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

            TestPollingConnectionMonitor monitor(logger);
            on_exit delete monitor;

            monitor.add(snrc);

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

        if (snopts.client_id && snopts.client_secret) {
            hash<auto> auth = {
                "username": snopts.username,
                "password": snopts.password,
                "client_id": snopts.client_id,
                "client_secret": snopts.client_secret,
                "oauth2_grant_type": "password",
            };

            # setup valid connection and test polling
            ServiceNowRestConnection snrc("test", "test", url, {"monitor": True}, auth);

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

            TestPollingConnectionMonitor monitor(logger);
            on_exit delete monitor;

            monitor.add(snrc);

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

    connectionTest() {
        string url = "snrests://localhost:8080";
        ServiceNowRestConnection swsc("test", "test", url, {}, {
            "oauth2_grant_type": "password",
            "client_id": "x",
            "client_secret": "x",
            "username": "x",
            "password": "x",
        });
        assertEq(True, swsc instanceof ServiceNowRestConnection);
        hash<auto> info = swsc.getInfo();
        assertEq(False, info.children_can_support_apis);
        assertEq(True, info.children_can_support_records);
        assertEq(False, info.children_can_support_observers);
        assertEq(False, info.children_can_support_messages);

        # ping negative test
        hash<PingInfo> ping = swsc.ping();
        assertEq("test", ping.name);
        assertFalse(ping.ok);
        assertEq("SOCKET-CONNECT-ERROR", ping.ex.err);

        assertThrows("CONNECTION-ERROR", sub () {
            ServiceNowRestConnection swsc1("test", "test", url, {"monitor": True}, {"x": 1});
        });

%ifdef NoJson
        testSkip("no json module present");
%endif

        ServiceNowRestClient client = swsc.get(False);
        # issue #3321: check default timeouts
        assertEq(45000, client.getTimeout());
        assertEq(45000, client.getConnectTimeout());

        swsc = new ServiceNowRestConnection("test", "test", url, {}, {
            "client_id": "x",
            "client_secret": "x",
            "username": "x",
            "password": "x",
        });
        assertEq("https://localhost:8080/oauth_auth.do?scope=&redirect_uri=auto&client_id=x&response_type=code",
            swsc.getAuthorizationCodeRequest());
    }
}

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"});
    }
}
