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

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

%include ../FtpServer.qc

%requires ../../../../../qlib/Util.qm
%requires ../../../../../qlib/QUnit.qm
%requires ../../../../../qlib/FsUtil.qm

%exec-class FtpTest

class FtpTest inherits QUnit::Test {
    private {
        # file on some public ftp
        const Url = "ftp://demo:password@test.rebex.net/readme.txt";

        # FtpClient url
        string url;

        # path on remote host
        string path;

        # file on remote host
        string file;

        # valid flag
        bool valid = True;

        # path to local test file
        string local_path;

        const MyOpts = Opts + (
            "port": "p,port=i",
        );

        const OptionColumn = 22;
    }

    constructor() : Test("FtpClient Test", "1.0", \ARGV, MyOpts) {
        # set valid flag; skip tests if no connection can be made
        url = ARGV[0] ?? ENV.QORE_FTPTEST_URL ?? Url;

        addTestCase("test issue 3031", \testIssue3031());
        addTestCase("test issue 3032", \testIssue3032());
        addTestCase("test issue 3033", \testIssue3033());
        addTestCase("test PASV", \testPasv());
        addTestCase("FtpClient class test case", \testFtpClient());
        addTestCase("FtpClient simulated server", \testFtpClientSim());

        local_path = createLocalFile();
        on_exit unlink(local_path);

        set_return_value(main());
    }

    private usageIntern() {
        TestReporter::usageIntern(OptionColumn);
        printOption("-p,--port=ARG", "set FTP server port", OptionColumn);
    }

    FtpClient getClient() {
        int tries = 0;
        while (True) {
            hash<UrlInfo> h = parse_url(url);
            if (!exists h.path)
                throw "FTP-ERROR", sprintf("url %y is missing a path to retrieve", url);
            if (!exists h.host)
                throw "FTP-ERROR", sprintf("url %y is missing the hostname", url);

            path = dirname(h.path);
            file = basename(h.path);

            FtpClient fc();
            fc.setURL(url);
            try {
                fc.connect();
                fc.disconnect();
            } catch (hash<ExceptionInfo> ex) {
                printf("%s: %s\n", ex.err, ex.desc);
                if (tries++ < 2) {
                    printf("retrying...");
                    continue;
                }
                rethrow;
            }
            return fc;
        }
    }

    string createLocalFile() {
        string local_path = tmp_location() + '/ftptest';

        File f();
        f.open2(local_path, O_CREAT|O_WRONLY|O_TRUNC);
        for (int i = 0; i < 200; ++i) {
            f.write("this is a test 1234567890\n");
        }

        return local_path;
    }

    int getPort() {
        int port;
        if (m_options.port)
            port = m_options.port;
        else if (ENV.FTPCLIENT_TEST_PORT)
            port = ENV.FTPCLIENT_TEST_PORT.toInt();
        else
            port = 0;

        return port;
    }

    testIssue3031() {
        int port = getPort();

        # PORT only supports IPv4, so we can't use "localhost", which may resolve
        # to an IPv6 address
        FtpServer serv(port, m_options.verbose, "127.0.0.1");
        on_exit serv.shutdown();

        port = serv.getPort();
        FtpClient fc("ftp://user:pass@127.0.0.1:" + port);
        Queue q();
        fc.setWarningQueue(20000, 20000, q, "test");
        assertNothing(fc.connect());

        fc.setModePORT();

        assertNothing(fc.put(local_path));

        serv.setBroken("PORT", 2);
        fc.setTimeout(1ms);
        assertEq(1, fc.getTimeout());

        assertThrows("SOCKET-TIMEOUT", \fc.put(), local_path);
    }

    testIssue3032() {
        int port = getPort();

        FtpServer serv(port, m_options.verbose);
        on_exit serv.shutdown();

        port = serv.getPort();
        FtpClient fc("ftp://user:pass@localhost:" + port);
        Queue q();
        fc.setWarningQueue(20000, 20000, q, "test");
        assertNothing(fc.connect());

        FileInputStream fis(local_path);
        assertNothing(fc.put(fis, "test_file.txt"));
    }

    testIssue3033() {
        # create empty file
        string empty_path = tmp_location() + DirSep + get_random_string();

        {
            File f();
            f.open2(empty_path, O_CREAT|O_WRONLY|O_TRUNC);
        }
        on_exit unlink(empty_path);

        int port = getPort();
        FtpServer serv(port, m_options.verbose);
        on_exit serv.shutdown();

        port = serv.getPort();
        FtpClient fc("ftp://user:pass@localhost:" + port);
        Queue q();
        fc.setWarningQueue(20000, 20000, q, "test");
        assertNothing(fc.connect());

        assertNothing(fc.put(empty_path));
    }

    testPasv() {
        int port = getPort();

        FtpServer serv(port, m_options.verbose);
        on_exit serv.shutdown();

        port = serv.getPort();
        FtpClient fc("ftp://user:pass@localhost:" + port);
        Queue q();
        fc.setWarningQueue(20000, 20000, q, "test");
        assertNothing(fc.connect());

        fc.setModePASV();

        assertNothing(fc.put(local_path));

        serv.setBroken("stor", 2s);
        fc.setTimeout(1ms);
        {
            hash<ExceptionInfo> ex;
            try {
                fc.put(local_path);
                assertTrue(False);
            } catch (hash<ExceptionInfo> ex1) {
                ex = ex1;
            }
            assertTrue(ex.err == "SOCKET-TIMEOUT" || ex.err == "FTP-RECEIVE-ERROR");
        }

        fc.setTimeout(60s);
        serv.setBroken("stor", 0);
        assertNothing(fc.put(local_path));

        assertNothing(fc.get(local_path, local_path));
        assertRegex("abc", ReadOnlyFile::readTextFile(local_path));

        serv.setBroken("retr", 2s);
        fc.setTimeout(1ms);
        {
            string err;
            try {
                fc.get(local_path, local_path);
            }
            catch (hash<ExceptionInfo> ex) {
                err = ex.err;
            }
            assertTrue(err == "SOCKET-TIMEOUT" || err == "FTP-RECEIVE-ERROR");
        }

        fc.setTimeout(60s);
        serv.setBroken("retr", 0);
        serv.setGetEmpty();
        assertNothing(fc.get(local_path, local_path));
        assertEq("", ReadOnlyFile::readTextFile(local_path));
        assertEq("", fc.getAsString(local_path));

        assertEq(Type::String, fc.nlst().type());
    }

    testFtpClient() {
        FtpClient fc;
        try {
            fc = getClient();
        } catch (hash<ExceptionInfo> ex) {
            testSkip(sprintf("FtpClient object is not valid: %s: %s", ex.err, ex.desc));
            return;
        }

        assertNothing(fc.connect());

        string local_read_path = tmp_location() + '/ftptest-read';
        unlink(local_read_path);

%ifdef Windows
        path =~ s/\\/\//g;
%endif

        fc.cwd(path);

        testAssertion("FtpClient::get()", \fc.get(), (file, local_read_path), new TestResultValue(NOTHING));

        *list l = stat(local_read_path);
        testAssertion("FtpClient::get() return value", int sub () {return l.typeCode();}, NOTHING, new TestResultValue(NT_LIST));

        testAssertion("FtpClient mode-1", \notEquals(), (fc.getMode(), "auto"));

        fc.disconnect();
        unlink(local_read_path);

        testAssertion("FtpClient mode-2", \fc.getMode(), NOTHING, new TestResultValue("auto"));
        fc.setModePASV();

        assertNothing(fc.connect());

        testAssertion("FtpClient mode-3", \fc.getMode(), NOTHING, new TestResultValue("pasv"));
        fc.disconnect();
        testAssertion("FtpClient mode-4", \fc.getMode(), NOTHING, new TestResultValue("pasv"));

        # test for issue #1295
        assertEq(False, fc.pwd() =~ /"/);
    }

    testFtpClientSim() {
        int port = getPort();

        FtpServer serv(port, m_options.verbose, "127.0.0.1");
        on_exit serv.shutdown();

        port = serv.getPort();
        FtpClient fc("ftp://user:pass@127.0.0.1:" + port);
        Queue q();
        fc.setWarningQueue(20000, 20000, q, "test");
        assertEq(AF_UNSPEC, fc.getNetworkFamily());
        assertThrows("FTPCLIENT-SETNETWORKFAMILY-ERROR", \fc.setNetworkFamily(), -1);

        fc.setNetworkFamily(AF_INET);
        assertEq(NOTHING, fc.connect());

        hash info = fc.getControlSocketInfo();
        assertEq(AF_INET, info.family);
        info = fc.getControlPeerInfo();
        assertEq(AF_INET, info.family);

        assertNothing(fc.put(local_path));

        serv.setBroken("stor", 2s);
        fc.setTimeout(1ms);
        {
            hash<ExceptionInfo> ex;
            try {
                fc.put(local_path);
                assertTrue(False);
            }
            catch (hash<ExceptionInfo> ex1) {
                ex = ex1;
            }
            assertTrue(ex.err == "SOCKET-TIMEOUT" || ex.err == "FTP-RECEIVE-ERROR");
        }

        fc.setTimeout(60s);
        serv.setBroken("stor", 0);
        assertNothing(fc.put(local_path));

        assertNothing(fc.get(local_path, local_path));
        assertEq(True, ReadOnlyFile::readTextFile(local_path) =~ /abc/);

        serv.setBroken("retr", 2s);
        fc.setTimeout(1ms);
        {
            string err;
            try {
                fc.get(local_path, local_path);
            }
            catch (hash<ExceptionInfo> ex) {
                err = ex.err;
            }
            assertTrue(err == "SOCKET-TIMEOUT" || err == "FTP-RECEIVE-ERROR");
        }

        fc.setTimeout(60s);
        serv.setBroken("retr", 0);
        serv.setGetEmpty();
        assertNothing(fc.get(local_path, local_path));
        assertEq("", ReadOnlyFile::readTextFile(local_path));
        assertEq("", fc.getAsString(local_path));

        assertEq(Type::String, fc.nlst().type());
    }
}
