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

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

%requires ../../../../qlib/QUnit.qm

%exec-class parseUrlTest

public class parseUrlTest inherits QUnit::Test {
    constructor() : Test("parse_url test", "1.0") {
        addTestCase("decode test", \decodeTest());
        addTestCase("slash test", \slashTest());
        addTestCase("Test 1", \test());
        addTestCase("port test", \portTest());

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

    decodeTest() {
        hash<UrlInfo> h = parse_url("https://user-%3cname%3e%40example.com%2f"
            "token:pass%3f%3aword@qoretechnologies.zendesk.com/api/v2/users%40str", QURL_DECODE);
        assertEq("user-<name>@example.com/token", h.username);
        assertEq("pass?:word", h.password);
        assertEq("qoretechnologies.zendesk.com", h.host);
        assertEq("/api/v2/users%40str", h.path);

        h = parse_url("https://user-%3cname%3e%40example.com%2f"
            "token:pass%3f%3aword@qoretechnologies.zendesk.com/api/v2/users%40str", QURL_DECODE_PATH);
        assertEq("user-<name>@example.com/token", h.username);
        assertEq("pass?:word", h.password);
        assertEq("qoretechnologies.zendesk.com", h.host);
        assertEq("/api/v2/users@str", h.path);
    }

    slashTest() {
        hash<auto> h = parse_url("https://user@example.com/token:api-token@qoretechnologies.zendesk.com/api/v2/users");
        assertEq("user@example.com/token", h.username);
        assertEq("api-token", h.password);
        assertEq("qoretechnologies.zendesk.com", h.host);
        assertEq("/api/v2/users", h.path);

        h = parse_url("https://user@example.com/token:api-token@qoretechnologies.zendesk.com");
        assertEq("user@example.com/token", h.username);
        assertEq("api-token", h.password);
        assertEq("qoretechnologies.zendesk.com", h.host);
        assertNothing(h.path);

        h = parse_url("https://user@example.com:token@qoretechnologies.zendesk.com");
        assertEq("user@example.com", h.username);
        assertEq("token", h.password);
        assertEq("qoretechnologies.zendesk.com", h.host);
        assertNothing(h.path);

        h = parse_url("https://host:1234?arg=true");
        assertEq("host", h.host);
        assertEq(1234, h.port);
        assertEq("?arg=true", h.path);
    }

    test() {
        # issue #4568
        hash<auto> h = parse_url("esmtptls://username@example.com:uWEb*4Z2!DW~Nr->UK~^?TrR$Y=@smtp.example.com:587");
        assertEq({
            "protocol": "esmtptls",
            "username": "username@example.com",
            "password": "uWEb*4Z2!DW~Nr->UK~^?TrR$Y=",
            "host": "smtp.example.com",
            "port": 587,
        }, h);

        h = parse_url("user:pass@site:1001/path");
        assertEq({
            "username": "user",
            "password": "pass",
            "host": "site",
            "port": 1001,
            "path": "/path",
        }, h);

        h = parse_url("user:pass@:1001/path");
        assertEq({
            "username": "user",
            "password": "pass",
            "port": 1001,
            "path": "/path",
        }, h);

        # issue #4673: pase_url() fails with passwords with a / char in the name
        h = parse_url("https://user:pass//X@host");
        assertEq({
            "protocol": "https",
            "username": "user",
            "password": "pass//X",
            "host": "host",
        }, h);

        h = parse_url("https://user:pass//X@host/path");
        assertEq({
            "protocol": "https",
            "username": "user",
            "password": "pass//X",
            "host": "host",
            "path": "/path",
        }, h);

        # windows paths according to https://learn.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows
        h = parse_url("file:///c:/dir1/dir2");
        assertEq({
            "protocol": "file",
            "path": "c:/dir1/dir2",
        }, h);

        # alternate format with backslashes
        h = parse_url("file:///c:\\dir1\\dir2");
        assertEq({
            "protocol": "file",
            "path": "c:\\dir1\\dir2",
        }, h);

        # "standard" test
        assertEq(("protocol": "http", "path": "/path", "username": "user", "password": "pass", "host": "host", "port": 80), parse_url("http://user:pass@host:80/path"));

        # "standard" path
        assertEq(("protocol": "file", "path": "/tmp"), parse_url("file:///tmp"));

        # UNIX socket URL
        assertEq(("protocol": "http", "host": "/tmp/socket-file.sock"), parse_url("http://socket=%2Ftmp%2Fsocket-file.sock"));
        assertEq(("protocol": "http", "host": "/tmp/socket-file.sock", "path": "/path"), parse_url("http://socket=%2Ftmp%2Fsocket-file.sock/path"));

        # windows file paths
        assertEq(("protocol": "file", "path": "c:\\tmp"), parse_url("file://c:\\tmp"));
        assertEq(("protocol": "file", "path": "\\\\share\\dir"), parse_url("file://\\\\share\\dir"));

        # IPv6 localhost
        assertEq(("host": "::"), parse_url("::"));

        # one-character username
        assertEq(("protocol": "http", "username": "u", "password": "p", "host": "h"), parse_url("http://u:p@h"));

        # check that we can parse a UTF-16-encoded URL
        assertEq(("protocol": "http", "username": "u", "password": "p", "host": "h"), parse_url(convert_encoding("http://u:p@h", "UTF-16")));

        # empty URL
        assertThrows("PARSE-URL-ERROR", sub () { parse_url(""); } );

        # invalid characters in port
        assertThrows("PARSE-URL-ERROR", sub () { parse_url("scheme://user@host:hi/"); } );

        # corner case: a path string should be recognized and parsed into the path key
        assertEq(("path": "/tmp/file"), parse_url("/tmp/file"));

        # corner case: a UNIX socket path string with no protocol
        assertEq(("host": "/tmp/socket-file.sock"), parse_url("socket=%2Ftmp%2Fsocket-file.sock"));

        # corner case: a UNIX socket path string with no protocol but with a path
        assertEq(("host": "/tmp/socket-file.sock", "path": "/path"), parse_url("socket=%2Ftmp%2Fsocket-file.sock/path"));

        # corner case: a relative path must be parsed with just the "path" key
        assertEq(("path": "some/path"), parse_url("some/path"));

        # corner case: a hostname must be returned just in the "host" key
        assertEq(("host": "hostname"), parse_url("hostname"));

        # corner case: a single leading backslash is returned as a hostname
        assertEq(("host": "\\hostname"), parse_url("\\hostname"));

        # corner case: two leading backslashes are returned as a UNC path
        assertEq(("path": "\\\\hostname"), parse_url("\\\\hostname"));

        # corner case: two leading forward slashes are returned as a UNC path
        assertEq(("path": "//hostname"), parse_url("//hostname"));

        # corner case: file:// with two leading backslashes are returned as a UNC path
        assertEq({
            "protocol": "file",
            "path": "\\\\hostname",
        }, parse_url("file://\\\\hostname"));

        # corner case: file:// with two leading forward slashes are returned as a UNC path
        assertEq({
            "protocol": "file",
            "path": "//hostname",
        }, parse_url("file:////hostname"));

        # issue #3287: single character hostname
        assertEq("a", parse_url("http://a").host);
        assertEq("a", parse_url("http://a/").host);
        assertEq("a", parse_url("http://a:1").host);
        assertEq("a", parse_url("http://a:1/").host);
    }

    private portTest() {
        # test URL with a port and a path with a colon
        assertEq(<UrlInfo>{
            "protocol": "https",
            "host": "host",
            "port": 8080,
            "path": "/something:other",
        }, parse_url("https://host:8080/something:other"));

        assertEq(<UrlInfo>{
            "protocol": "https",
            "host": "host",
            "port": 8080,
            "path": "/something:1234",
        }, parse_url("https://host:8080/something:1234"));

        # non num chars in port
        try {
            parse_url("http://localhost:abc");
            fail("it must throw an exception");
        } catch (hash<ExceptionInfo> ex) {
            assertEq("PARSE-URL-ERROR", ex.err);
            assertEq(True, ex.desc =~ /has an invalid non-numeric character in the port specification/, ex.desc);
        }

        # out of INT range port
        try {
            parse_url("http://localhost:999999999999999");
            fail("it must throw an exception");
        } catch (hash<ExceptionInfo> ex) {
            assertEq("PARSE-URL-ERROR", ex.err);
            assertRegex("invalid port.*between.*0.*65535", ex.desc);
        }

        # out of int16 range: 0-65535
        try {
            parse_url("http://localhost:65536");
            fail("it must throw an exception");
        } catch (hash<ExceptionInfo> ex) {
            assertEq("PARSE-URL-ERROR", ex.err);
            assertRegex("invalid port.*between.*0.*65535", ex.desc);
        }
    }
}
