#!/usr/bin/env qore

%requires Util

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

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

%exec-class CopyTreeTest

public class CopyTreeTest inherits QUnit::Test {

    constructor() : Test ("CopyTreeTest", "1.0") {
        addTestCase("copy_tree test", \test_copy_tree());
        addTestCase("copy_tree overwrite test", \test_copy_tree_overwrite());
        addTestCase("copy_tree exception test", \test_copy_tree_exception());
        set_return_value(main());
    }

    test_copy_tree() {
        # create a temporary directory and a test directory tree in it
        string tmp_root = make_tmp_dir();
        on_exit {
            remove_tree(tmp_root);
        }
        string source_path = join_paths(tmp_root, "src");
        mkdir(source_path);
        string dir_path = join_paths(source_path, "dir");
        mkdir(dir_path);
        File tmp_file = new File();
        string file_path = join_paths(source_path, "file");
        tmp_file.open(file_path, O_CREAT, 0600);
        tmp_file.close();
        string link_path = join_paths(dir_path, "link");
        symlink(file_path, link_path);

        # now we should have something like...
        # tmp_root
        # └── src
        #     ├── dir
        #     │   └── link -> tmp_root/src/file
        #     └── file
        # ... let's check it
        assertTrue(path_exists(source_path));
        assertTrue(is_dir(source_path));
        assertTrue(path_exists(dir_path));
        assertTrue(is_dir(dir_path));
        assertTrue(path_exists(file_path));
        assertTrue(is_file(file_path));
        assertTrue(path_exists(link_path));
        assertTrue(is_link(link_path));
        assertEq(file_path, readlink(link_path));

        # let's copy the source directory tree and check the result too
        string dst_path = join_paths(tmp_root, "dst");
        string cp_dest_path = copy_tree(source_path, dst_path);
        assertEq(dst_path, cp_dest_path);
        # this is what we expect
        string dst_dir_path = join_paths(dst_path, "dir");
        string dst_file_path = join_paths(dst_path, "file");
        string dst_link_path = join_paths(dst_dir_path, "link");
        assertTrue(path_exists(dst_path));
        assertTrue(is_dir(dst_path));
        assertTrue(path_exists(dst_dir_path));
        assertTrue(is_dir(dst_dir_path));
        assertTrue(path_exists(dst_file_path));
        assertTrue(is_file(dst_file_path));
        assertTrue(path_exists(dst_link_path));
        assertTrue(is_link(dst_link_path));
        assertEq(file_path, readlink(dst_link_path));  # follow_symlinks=False
        # let's clean it
        remove_tree(dst_path);
        assertFalse(path_exists(dst_path));

        # let's try again but this time use an existing directory as destination
        mkdir(dst_path);
        cp_dest_path = copy_tree(source_path, dst_path);
        string new_dst_path = join_paths(dst_path, basename(source_path));
        string new_dst_dir_path = join_paths(new_dst_path, "dir");
        string new_dst_file_path = join_paths(new_dst_path, "file");
        string new_dst_link_path = join_paths(new_dst_dir_path, "link");
        assertEq(new_dst_path, cp_dest_path);
        assertTrue(path_exists(new_dst_path));
        assertTrue(is_dir(new_dst_path));
        assertTrue(path_exists(new_dst_dir_path));
        assertTrue(is_dir(new_dst_dir_path));
        assertTrue(path_exists(new_dst_file_path));
        assertTrue(is_file(new_dst_file_path));
        assertTrue(path_exists(new_dst_link_path));
        assertTrue(is_link(new_dst_link_path));
        assertEq(file_path, readlink(new_dst_link_path));  # follow_symlinks=False
        # let's clean it
        remove_tree(dst_path);
        assertFalse(path_exists(dst_path));

        # let's try again with follow_symlinks=True
        mkdir(dst_path);
        cp_dest_path = copy_tree(source_path, dst_path, True, True);
        assertEq(dst_path, cp_dest_path);
        assertTrue(path_exists(dst_path));
        assertTrue(is_dir(dst_path));
        assertTrue(path_exists(dst_dir_path));
        assertTrue(is_dir(dst_dir_path));
        assertTrue(path_exists(dst_file_path));
        assertTrue(is_file(dst_file_path));
        assertTrue(path_exists(dst_link_path));
        assertTrue(is_file(dst_link_path));  # follow_symlinks=True
        # let's clean it
        remove_tree(dst_path);
        assertFalse(path_exists(dst_path));

        # now let's copy it via a link to the source
        # let's try again with follow_symlinks=True
        string src_link_path = join_paths(tmp_root, "src_link");
        assertFalse(path_exists(src_link_path));
        symlink(source_path, src_link_path);
        assertTrue(path_exists(src_link_path));
        assertTrue(is_link(src_link_path));
        cp_dest_path = copy_tree(source_path, dst_path, True);
        assertEq(dst_path, cp_dest_path);
        assertTrue(path_exists(dst_path));
        assertTrue(is_dir(dst_path));
        assertTrue(path_exists(dst_dir_path));
        assertTrue(is_dir(dst_dir_path));
        assertTrue(path_exists(dst_file_path));
        assertTrue(is_file(dst_file_path));
        assertTrue(path_exists(dst_link_path));
        assertTrue(is_file(dst_link_path));  # follow_symlinks=True
    }

    test_copy_tree_overwrite() {
        # create a temporary directory and a test directory tree in it
        TmpDir tmp_root = new TmpDir();
        string source_path = join_paths(tmp_root.path, "src");
        mkdir(source_path);
        string dir_path = join_paths(source_path, "dir");
        mkdir(dir_path);
        File tmp_file = new File();
        string file_path = join_paths(source_path, "file");
        tmp_file.open(file_path, O_CREAT, 0600);
        tmp_file.close();

        # now we should have something like...
        # tmp_root
        # └── src
        #     ├── dir
        #     └── file
        # ... let's check it
        assertTrue(path_exists(source_path));
        assertTrue(is_dir(source_path));
        assertTrue(path_exists(dir_path));
        assertTrue(is_dir(dir_path));
        assertTrue(path_exists(file_path));
        assertTrue(is_file(file_path));

        # let's copy the source directory tree and check the result too
        string dst_path = join_paths(tmp_root.path, "dst");
        string cp_dest_path = copy_tree(source_path, dst_path);
        assertEq(dst_path, cp_dest_path);
        # this is what we expect
        string dst_dir_path = join_paths(dst_path, "dir");
        string dst_file_path = join_paths(dst_path, "file");
        assertTrue(path_exists(dst_path));
        assertTrue(is_dir(dst_path));
        assertTrue(path_exists(dst_dir_path));
        assertTrue(is_dir(dst_dir_path));
        assertTrue(path_exists(dst_file_path));
        assertTrue(is_file(dst_file_path));

        # copy with overwrite
        cp_dest_path = copy_tree(source_path, dst_path, NOTHING, True);
        assertEq(dst_path, cp_dest_path);
        # everything is copied anew -> let's check it
        dst_dir_path = join_paths(dst_path, "dir");
        dst_file_path = join_paths(dst_path, "file");
        assertTrue(path_exists(dst_path));
        assertTrue(is_dir(dst_path));
        assertTrue(path_exists(dst_dir_path));
        assertTrue(is_dir(dst_dir_path));
        assertTrue(path_exists(dst_file_path));
        assertTrue(is_file(dst_file_path));

        # now let's add something to the destination path and then overwrite with a new copy
        string new_dir_dst = join_paths(dst_path, "new_dir");
        mkdir(new_dir_dst);
        assertTrue(path_exists(new_dir_dst));
        assertTrue(is_dir(new_dir_dst));
        # let's overwrite destination now
        cp_dest_path = copy_tree(source_path, dst_path, NOTHING, True);
        assertEq(dst_path, cp_dest_path);
        # everything is copied anew -> let's check it
        dst_dir_path = join_paths(dst_path, "dir");
        dst_file_path = join_paths(dst_path, "file");
        assertTrue(path_exists(dst_path));
        assertTrue(is_dir(dst_path));
        assertTrue(path_exists(dst_dir_path));
        assertTrue(is_dir(dst_dir_path));
        assertTrue(path_exists(dst_file_path));
        assertTrue(is_file(dst_file_path));
        # and check that the new dir is not there
        assertFalse(path_exists(new_dir_dst));
        assertFalse(is_dir(new_dir_dst));

        # now let's add something to the source path and then overwrite destination again
        string new_dir_src = join_paths(source_path, "new_dir");
        mkdir(new_dir_src);
        assertTrue(path_exists(new_dir_src));
        assertTrue(is_dir(new_dir_src));
        assertFalse(path_exists(new_dir_dst));
        assertFalse(is_dir(new_dir_dst));

        # let's overwrite destination now
        cp_dest_path = copy_tree(source_path, dst_path, NOTHING, True);
        assertEq(dst_path, cp_dest_path);
        # everything is copied anew -> let's check it
        dst_dir_path = join_paths(dst_path, "dir");
        dst_file_path = join_paths(dst_path, "file");
        assertTrue(path_exists(dst_path));
        assertTrue(is_dir(dst_path));
        assertTrue(path_exists(dst_dir_path));
        assertTrue(is_dir(dst_dir_path));
        assertTrue(path_exists(dst_file_path));
        assertTrue(is_file(dst_file_path));
        # and check that the new dir is there as well
        assertTrue(path_exists(new_dir_dst));
        assertTrue(is_dir(new_dir_dst));
    }

    test_copy_tree_exception() {
        # create a temporary directory and a test directory tree in it
        string tmp_root = make_tmp_dir();
        on_exit {
            remove_tree(tmp_root);
        }
        string source_path = join_paths(tmp_root, "src");
        mkdir(source_path);
        string dir_path = join_paths(source_path, "dir");
        mkdir(dir_path);
        File tmp_file = new File();
        string file_path = join_paths(source_path, "file");
        tmp_file.open(file_path, O_CREAT, 0600);
        tmp_file.close();
        string link_path = join_paths(dir_path, "link");
        symlink(file_path, link_path);
        string dest_dir = join_paths(tmp_root, "dest");
        mkdir(dest_dir);

        # now we should have something like...
        # tmp_root
        # ├── dest
        # └── src
        #     ├── dir
        #     │   └── link -> tmp_root/src/file
        #     └── file
        # ... let's check it
        assertTrue(path_exists(source_path));
        assertTrue(is_dir(source_path));
        assertTrue(path_exists(dir_path));
        assertTrue(is_dir(dir_path));
        assertTrue(path_exists(file_path));
        assertTrue(is_file(file_path));
        assertTrue(path_exists(link_path));
        assertTrue(is_link(link_path));
        assertEq(file_path, readlink(link_path));
        assertTrue(path_exists(dest_dir));
        assertTrue(is_dir(dest_dir));

        # try and copy non-existing directory
        string non_existing = join_paths(tmp_root, "non-existing");
        assertThrows("DIR-STAT-ERROR", \copy_tree(), (non_existing, dest_dir));

        # try and copy a file instead of a directory
        assertThrows("DIR-READ-FAILURE", \copy_tree(), (file_path, dest_dir, NOTHING, True));
        # without overwrite it will complain about destination path existing instead
        assertThrows("PATH-EXISTS-ERROR", \copy_tree(), (file_path, dest_dir));

        # try and copy a directory into itself
        assertThrows("SAME-DIR-ERROR", \copy_tree(), (dest_dir, dest_dir));

        # now let's make tmp_root/src/file unreadable -> copying it should fail
        chmod(file_path, 0000);
        bool exception_thrown = False;

        # with overwrite it will fail because of the permissions
        assertThrows("FILE-OPEN2-ERROR", \copy_tree(), (source_path, dest_dir, NOTHING, True));

        # and now with fail_immediately=False COPY-TREE-ERROR will be thrown
        try {
            copy_tree(source_path, dest_dir, NOTHING, True, False);
        } catch (hash<ExceptionInfo> err) {
            exception_thrown = True;
            assertEq("COPY-TREE-ERROR", err.err);
            assertEq(1, err.desc.size());
            assertEq("FILE-OPEN2-ERROR", err.desc[0].err);
            assertGt(-1, index(err.desc[0].desc, file_path));
        }
        assertTrue(exception_thrown);
        remove_tree(dest_dir);

        # try that again with follow_symlinks (i.e. the link will also be unreadable)
        assertThrows("FILE-OPEN2-ERROR", \copy_tree(), (source_path, dest_dir, True));
        remove_tree(dest_dir);

        # and now with fail_immediately=False COPY-TREE-ERROR will be thrown
        exception_thrown = False;
        try {
            copy_tree(source_path, dest_dir, True, NOTHING, False);
        } catch (hash<ExceptionInfo> err) {
            exception_thrown = True;
            assertEq("COPY-TREE-ERROR", err.err);
            assertEq(2, err.desc.size());
            assertEq("FILE-OPEN2-ERROR", err.desc[0].err);
            assertEq("FILE-OPEN2-ERROR", err.desc[1].err);
            assertGt(-1, index(err.desc[0].desc, file_path));
            assertGt(-1, index(err.desc[1].desc, file_path));
        }
        assertTrue(exception_thrown);
        remove_tree(dest_dir);

        # let's repeat the last two tests with fail_immediately flag
        assertThrows("FILE-OPEN2-ERROR", \copy_tree(),
                     (source_path, dest_dir, NOTHING, NOTHING, True));
        remove_tree(dest_dir);

        assertThrows("FILE-OPEN2-ERROR", \copy_tree(),
                     (source_path, dest_dir, True, NOTHING, True));
        remove_tree(dest_dir);
    }
}