/*
 * Ceph - scalable distributed file system
 *
 * Copyright (C) 2022 Cloudbase Solutions
 *
 * This is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1, as published by the Free Software
 * Foundation.  See file COPYING.
 *
 */

#include <windows.h>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <sys/socket.h>
#include <direct.h>

#include "gtest/gtest.h"

#include "common/SubProcess.h"
#include "common/run_cmd.h"
#include "include/uuid.h"

#define DEFAULT_MOUNTPOINT "X:\\"
#define MOUNT_POLL_ATTEMPT 10
#define MOUNT_POLL_INTERVAL_MS 1000
#define TEST_VOL_SERIAL "1234567890"
#define MByte 1048576

namespace fs = std::filesystem;
using namespace std::chrono_literals;

std::string get_uuid() {
    uuid_d suffix;
    suffix.generate_random();

    return suffix.to_string();
}

bool move_eof(HANDLE handle, LARGE_INTEGER offset) {

    // Move file pointer to FILE_BEGIN + offset
    if (!SetFilePointerEx(handle, offset, NULL, FILE_BEGIN)) {
        std::cerr << "Setting file pointer failed. err: "
                  << GetLastError() << std::endl;
        return false;
    }

    if (!SetEndOfFile(handle)) {
        std::cerr << "Setting EOF failed. err: " << GetLastError() << std::endl;
        return false;
    }

    return true;
}

void write_file(std::string file_path, std::string data) {
    std::ofstream file;
    file.open(file_path);

    ASSERT_TRUE(file.is_open())
        << "Failed to open file: " << file_path;
    file << data;
    file.flush();

    file.close();
}

void expect_write_failure(std::string file_path) {
    std::ofstream file;
    file.open(file_path);

    ASSERT_FALSE(file.is_open());
}

std::string read_file(std::string file_path) {
    std::ifstream file;
    file.open(file_path);
    std::string content((std::istreambuf_iterator<char>(file)),
                         std::istreambuf_iterator<char>());
    file.close();

    return content;
}

void check_write_file(std::string file_path, std::string data) {
    write_file(file_path, data);
    ASSERT_EQ(read_file(file_path), data);
}

int wait_for_mount(std::string mount_path) {
    std::cerr << "Waiting for mount: " << mount_path << std::endl;

    int attempts = 0;
    do {
        attempts++;
        if (attempts < MOUNT_POLL_ATTEMPT)
            Sleep(MOUNT_POLL_INTERVAL_MS);
    } while (!fs::exists(mount_path)
             && attempts < MOUNT_POLL_ATTEMPT);

    if (!fs::exists(mount_path)) {
        std::cerr << "Timed out waiting for ceph-dokan mount: " 
                  << mount_path << std::endl;
        return -ETIMEDOUT;
    }

    std::cerr << "Successfully mounted: " << mount_path << std::endl;

    return 0;
}

void map_dokan(SubProcess** mount, const char* mountpoint) {
    SubProcess* new_mount = new SubProcess("ceph-dokan");

    new_mount->add_cmd_args("map", "--win-vol-name", "TestCeph",
                            "--win-vol-serial", TEST_VOL_SERIAL,
                            "-l", mountpoint, NULL);

    *mount = new_mount;
    ASSERT_EQ(new_mount->spawn(), 0);
    ASSERT_EQ(wait_for_mount(mountpoint), 0);
}

void map_dokan_read_only(
    SubProcess** mount,
    const char* mountpoint
) {
    SubProcess* new_mount = new SubProcess("ceph-dokan");
    new_mount->add_cmd_args("map", "--win-vol-name", "TestCeph",
                            "--win-vol-serial", TEST_VOL_SERIAL,
                            "--read-only", "-l", mountpoint, NULL);

    *mount = new_mount;
    ASSERT_EQ(new_mount->spawn(), 0);
    ASSERT_EQ(wait_for_mount(mountpoint), 0);
    std::cerr << mountpoint << " mounted in read-only mode"
              << std::endl;
}

void map_dokan_with_maxpath(
    SubProcess** mount,
    const char* mountpoint,
    uint64_t max_path_len)
{
    SubProcess* new_mount = new SubProcess("ceph-dokan");
    new_mount->add_cmd_args("map", "--debug", "--dokan-stderr",
                            "--win-vol-name", "TestCeph",
                            "--win-vol-serial", TEST_VOL_SERIAL,
                            "--max-path-len",
                            (std::to_string(max_path_len)).c_str(),
                            "-l", mountpoint, NULL);

    *mount = new_mount;
    ASSERT_EQ(new_mount->spawn(), 0);
    if (256 <= max_path_len && max_path_len <= 4096) {
        ASSERT_EQ(wait_for_mount(mountpoint), 0);
    } else {
        ASSERT_NE(wait_for_mount(mountpoint), 0);
    }
}

void unmap_dokan(SubProcess* mount, const char* mountpoint) {
    std::string ret = run_cmd("ceph-dokan", "unmap", "-l",
                              mountpoint, (char*)NULL);
                              
    ASSERT_EQ(ret, "") << "Failed unmapping: " << mountpoint;
    std::cerr<< "Unmounted: " << mountpoint << std::endl;

    ASSERT_EQ(mount->join(), 0);
}

int get_volume_max_path(std::string mountpoint){
    char volume_name[MAX_PATH + 1] = { 0 };
    char file_system_name[MAX_PATH + 1] = { 0 };
    DWORD serial_number = 0;
    DWORD max_component_len = 0;
    DWORD file_system_flags = 0;
    if (GetVolumeInformation(
            mountpoint.c_str(),
            volume_name,
            sizeof(volume_name),
            &serial_number,
            &max_component_len,
            &file_system_flags,
            file_system_name,
            sizeof(file_system_name)) != TRUE) {
        std::cerr << "GetVolumeInformation() failed, error: "
                  << GetLastError() << std::endl;
    }

    return max_component_len;
}

static SubProcess* shared_mount = nullptr;

class DokanTests : public testing::Test
{
protected:

    static void SetUpTestSuite() {
        map_dokan(&shared_mount, DEFAULT_MOUNTPOINT);
    }

    static void TearDownTestSuite() {
        if (shared_mount) {
            unmap_dokan(shared_mount, DEFAULT_MOUNTPOINT);
        }
        shared_mount = nullptr;
    }
};

TEST_F(DokanTests, test_mount) {
    std::string mountpoint = "Y:\\";
    SubProcess* mount = nullptr;
    map_dokan(&mount, mountpoint.c_str());
    unmap_dokan(mount, mountpoint.c_str());
}

TEST_F(DokanTests, test_mount_read_only) {
    std::string mountpoint = "Z:\\";
    std::string data = "abc123";
    std::string success_file_path = "ro_success_" + get_uuid();
    std::string failed_file_path = "ro_fail_" + get_uuid();

    SubProcess* mount = nullptr;
    map_dokan(&mount, mountpoint.c_str());

    check_write_file(mountpoint + success_file_path, data);
    ASSERT_TRUE(fs::exists(mountpoint + success_file_path));

    unmap_dokan(mount, mountpoint.c_str());

    mount = nullptr;
    map_dokan_read_only(&mount, mountpoint.c_str());

    expect_write_failure(mountpoint + failed_file_path);
    ASSERT_FALSE(fs::exists(mountpoint + failed_file_path));

    ASSERT_TRUE(fs::exists(mountpoint + success_file_path));
    ASSERT_EQ(read_file(mountpoint + success_file_path), data);

    std::string exception_msg(
        "filesystem error: cannot remove: No such device ["
        + mountpoint + success_file_path + "]");
    EXPECT_THROW({
        try {
            fs::remove(mountpoint + success_file_path);
        } catch(const fs::filesystem_error &e) {
            EXPECT_STREQ(e.what(), exception_msg.c_str());
            throw;
        }
    }, fs::filesystem_error);
    unmap_dokan(mount, mountpoint.c_str());

    map_dokan(&mount, mountpoint.c_str());
    ASSERT_TRUE(fs::exists(mountpoint + success_file_path));
    ASSERT_TRUE(fs::remove(mountpoint + success_file_path));
    unmap_dokan(mount, mountpoint.c_str());
}

TEST_F(DokanTests, test_delete_on_close) {
    std::string file_path = DEFAULT_MOUNTPOINT"file_" + get_uuid();
    HANDLE hFile = CreateFile(
        file_path.c_str(),
        GENERIC_WRITE, // open for writing
        0,             // sharing mode, none in this case
        0,             // use default security descriptor
        CREATE_NEW,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE,
        0);

    ASSERT_NE(hFile, INVALID_HANDLE_VALUE)
        << "Could not open file: "
        << DEFAULT_MOUNTPOINT"test_create.txt "
        << "err: " << GetLastError() << std::endl;
        
    ASSERT_NE(CloseHandle(hFile), 0);
    
    // FILE_FLAG_DELETE_ON_CLOSE is used
    ASSERT_FALSE(fs::exists(file_path));
}

TEST_F(DokanTests, test_io) {
    std::string data = "abcdef";
    std::string file_path = "test_io_" + get_uuid();

    std::string mountpoint = "I:\\";
    SubProcess* mount = nullptr;
    map_dokan(&mount, mountpoint.c_str());

    check_write_file(mountpoint + file_path, data);
    ASSERT_TRUE(fs::exists(mountpoint + file_path));
    unmap_dokan(mount, mountpoint.c_str());

    mountpoint = "O:\\";
    mount = nullptr;
    map_dokan(&mount, mountpoint.c_str());

    ASSERT_TRUE(fs::exists(mountpoint + file_path));
    EXPECT_EQ(data, read_file(mountpoint + file_path));
    ASSERT_TRUE(fs::remove((mountpoint + file_path).c_str()));
    ASSERT_FALSE(fs::exists(mountpoint + file_path));
    unmap_dokan(mount, mountpoint.c_str());
}

TEST_F(DokanTests, test_subfolders) {
    std::string base_dir_path = DEFAULT_MOUNTPOINT"base_dir_"
                                + get_uuid() + "\\";
    std::string sub_dir_path = base_dir_path
                               + "test_sub_dir" + get_uuid();
    std::string base_dir_file = base_dir_path 
                                + "file_" + get_uuid();
    std::string sub_dir_file = sub_dir_path 
                                + "file_" + get_uuid();

    std::string data = "abc";

    ASSERT_EQ(fs::create_directory(base_dir_path), true);
    ASSERT_TRUE(fs::exists(base_dir_path));

    ASSERT_EQ(fs::create_directory(sub_dir_path), true);
    ASSERT_TRUE(fs::exists(sub_dir_path));

    check_write_file(base_dir_file, data);
    ASSERT_TRUE(fs::exists(base_dir_file));

    check_write_file(sub_dir_file, data);
    ASSERT_TRUE(fs::exists(sub_dir_file));

    ASSERT_TRUE(fs::remove((sub_dir_file).c_str()))
        << "Failed to remove file: " << sub_dir_file;
    ASSERT_FALSE(fs::exists(sub_dir_file));

    // Remove empty dir
    ASSERT_TRUE(fs::remove((sub_dir_path).c_str()))
        << "Failed to remove directory: " << sub_dir_path;
    ASSERT_FALSE(fs::exists(sub_dir_file));

    ASSERT_NE(fs::remove_all((base_dir_path).c_str()), 0)
        << "Failed to remove directory: " << base_dir_path;
    ASSERT_FALSE(fs::exists(sub_dir_file));
}

TEST_F(DokanTests, test_find_files) {
    std::string basedir_path = "X:/find_" + get_uuid();
    std::string subdir_path = basedir_path + "/dir_" + get_uuid();
    std::string file1_path = basedir_path + "/file1_" + get_uuid();
    std::string file2_path = subdir_path + "/file2_" + get_uuid();

    ASSERT_TRUE(
        fs::create_directories(subdir_path)
    );

    std::ofstream{file1_path};
    std::ofstream{file2_path};

    std::vector<std::string> paths;

    for (const auto & entry : 
         fs::recursive_directory_iterator(basedir_path)
    ) {
        paths.push_back(entry.path().generic_string());
    }

    ASSERT_NE(std::find(begin(paths), end(paths), subdir_path), end(paths));
    ASSERT_NE(std::find(begin(paths), end(paths), file1_path), end(paths));
    ASSERT_NE(std::find(begin(paths), end(paths), file2_path), end(paths));

    // clean-up
    ASSERT_NE(fs::remove_all(basedir_path), 0);
}

TEST_F(DokanTests, test_move_file) {
    std::string dir1_path = DEFAULT_MOUNTPOINT
                            "test_mv_1_" + get_uuid() + "\\";
    std::string dir2_path = DEFAULT_MOUNTPOINT
                            "test_mv_2_" + get_uuid() + "\\";
    std::string file_name = "mv_file_" + get_uuid();
    std::string data = "abcd";

    ASSERT_TRUE(fs::create_directory(dir1_path));
    ASSERT_TRUE(fs::create_directory(dir2_path));

    check_write_file(dir1_path + file_name, data);

    fs::rename(dir1_path + file_name, dir2_path + file_name);

    ASSERT_TRUE(fs::exists(dir2_path + file_name));
    ASSERT_FALSE(fs::exists(dir1_path + file_name));

    ASSERT_EQ(data, read_file(dir2_path + file_name));

    // clean-up
    ASSERT_NE(fs::remove_all(dir1_path),0);
    ASSERT_NE(fs::remove_all(dir2_path),0);
}

TEST_F(DokanTests, test_max_path) {
    std::string mountpoint = "P:\\";
    std::string extended_mountpoint = "\\\\?\\" + mountpoint;
    SubProcess* mount = nullptr;
    char dir[200] = { 0 };
    char file[200] = { 0 };
    std::string data = "abcd1234";

    memset(dir, 'd', sizeof(dir) - 1);
    memset(file, 'f', sizeof(file) - 1);

    uint64_t max_path_len = 4096;
    
    map_dokan_with_maxpath(&mount,
                           mountpoint.c_str(),
                           max_path_len);
    EXPECT_EQ(get_volume_max_path(extended_mountpoint),
              max_path_len);

    std::string long_dir_path = extended_mountpoint;

    std::string dir_names[15];

    for (int i = 0; i < 15; i++) {
        std::string crt_dir = std::string(dir) + "_"
                              + get_uuid() + "\\";
        long_dir_path.append(crt_dir);
        int stat = _mkdir(long_dir_path.c_str());
        ASSERT_EQ(stat, 0) << "Error creating directory " << i
                           << ": " << GetLastError() << std::endl;
        dir_names[i] = crt_dir;
    }
    std::string file_path = long_dir_path + "\\" + std::string(file)
                            + "_" + get_uuid();

    check_write_file(file_path, data);

    // clean-up
    // fs::remove is unable to handle long Windows paths
    EXPECT_NE(DeleteFileA(file_path.c_str()), 0);

    for (int i = 14; i >= 0; i--) {
        std::string remove_dir = extended_mountpoint;
        for (int j = 0; j <= i; j++) {
            remove_dir.append(dir_names[j]);
        }
        
        EXPECT_NE(RemoveDirectoryA(remove_dir.c_str()), 0);
    }

    unmap_dokan(mount, mountpoint.c_str());

    // value exceeds 32767, so a failure is expected
    max_path_len = 32770;
    map_dokan_with_maxpath(&mount,
                           mountpoint.c_str(),
                           max_path_len);
    ASSERT_FALSE(fs::exists(mountpoint));

    // value is below 256, so a failure is expected
    max_path_len = 150;
    map_dokan_with_maxpath(&mount,
                           mountpoint.c_str(),
                           max_path_len);
    ASSERT_FALSE(fs::exists(mountpoint));

    // default value
    map_dokan(&mount, mountpoint.c_str());
    EXPECT_EQ(get_volume_max_path(mountpoint.c_str()), 256);

    unmap_dokan(mount, mountpoint.c_str());
}

TEST_F(DokanTests, test_set_eof) {
    std::string file_path = DEFAULT_MOUNTPOINT"test_eof_"
                            + get_uuid();
    HANDLE hFile = CreateFile(
        file_path.c_str(),
        GENERIC_WRITE, // open for writing
        0,             // sharing mode, none in this case
        0,             // use default security descriptor
        CREATE_NEW,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE,
        0);

    ASSERT_NE(hFile, INVALID_HANDLE_VALUE)
        << "Could not open file: "
        << DEFAULT_MOUNTPOINT"test_create.txt "
        << "err: " << GetLastError() << std::endl;

    LARGE_INTEGER offset;
    offset.QuadPart = 2 * MByte; // 2MB

    LARGE_INTEGER file_size;

    ASSERT_TRUE(move_eof(hFile, offset));
    ASSERT_NE(GetFileSizeEx(hFile, &file_size), 0);
    EXPECT_EQ(file_size.QuadPart, offset.QuadPart);

    offset.QuadPart = MByte; // 1MB

    ASSERT_TRUE(move_eof(hFile, offset));
    ASSERT_NE(GetFileSizeEx(hFile, &file_size), 0);
    EXPECT_EQ(file_size.QuadPart, offset.QuadPart);

    ASSERT_NE(CloseHandle(hFile), 0);
    
    // FILE_FLAG_DELETE_ON_CLOSE is used
    ASSERT_FALSE(fs::exists(file_path));
}

TEST_F(DokanTests, test_set_alloc_size) {
    std::string file_path = DEFAULT_MOUNTPOINT"test_alloc_size_"
                            + get_uuid();
    HANDLE hFile = CreateFile(
        file_path.c_str(),
        GENERIC_WRITE, // open for writing
        0,             // sharing mode, none in this case
        0,             // use default security descriptor
        CREATE_NEW,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE,
        0);

    ASSERT_NE(hFile, INVALID_HANDLE_VALUE)
        << "Could not open file: "
        << DEFAULT_MOUNTPOINT"test_create.txt "
        << "err: " << GetLastError() << std::endl;
    
    LARGE_INTEGER li;
    li.QuadPart = MByte;
    FILE_ALLOCATION_INFO fai;
    fai.AllocationSize = li;
    
    ASSERT_NE(SetFileInformationByHandle(
                hFile,
                FileAllocationInfo,
                &fai,
                sizeof(FILE_ALLOCATION_INFO)
             ),0) << "Error: " << GetLastError();

    LARGE_INTEGER offset;
    offset.QuadPart = 2 * MByte;
    LARGE_INTEGER file_size;

    ASSERT_TRUE(move_eof(hFile, offset));
    ASSERT_NE(GetFileSizeEx(hFile, &file_size), 0);
    EXPECT_EQ(file_size.QuadPart, offset.QuadPart);

    offset.QuadPart = MByte;

    ASSERT_TRUE(move_eof(hFile, offset));
    ASSERT_NE(GetFileSizeEx(hFile, &file_size), 0);
    EXPECT_EQ(file_size.QuadPart, offset.QuadPart);

    ASSERT_NE(CloseHandle(hFile), 0);
    
    // FILE_FLAG_DELETE_ON_CLOSE is used
    ASSERT_FALSE(fs::exists(file_path));
}

TEST_F(DokanTests, test_file_type) {
    std::string test_dir = DEFAULT_MOUNTPOINT"test_info_"
                            + get_uuid() + "\\";
    std::string file_path = test_dir + "file_"
                            + get_uuid();
    std::string dir_path = test_dir + "dir_"
                           + get_uuid() + "\\";

    ASSERT_TRUE(fs::create_directory(test_dir));

    std::ofstream{file_path};
    ASSERT_TRUE(fs::create_directory(dir_path));

    ASSERT_TRUE(fs::is_regular_file(fs::status(file_path)));
    ASSERT_TRUE(fs::is_directory(fs::status(dir_path)));

    // clean-up
    fs::remove_all(test_dir);

}

TEST_F(DokanTests, test_volume_info) {
    char volume_name[MAX_PATH + 1] = { 0 };
    char file_system_name[MAX_PATH + 1] = { 0 };
    DWORD serial_number = 0;
    DWORD max_component_len = 0;
    DWORD file_system_flags = 0;

    ASSERT_EQ(
        GetVolumeInformation(
            DEFAULT_MOUNTPOINT,
            volume_name,
            sizeof(volume_name),
            &serial_number,
            &max_component_len,
            &file_system_flags,
            file_system_name,
            sizeof(file_system_name)),TRUE) 
        << "GetVolumeInformation() failed, error: "
        << GetLastError() << std::endl;

    ASSERT_STREQ(volume_name, "TestCeph") 
        << "Received: " << volume_name << std::endl;
    ASSERT_STREQ(file_system_name, "Ceph")
        << "Received: " << file_system_name << std::endl;
    ASSERT_EQ(max_component_len, 256);
    ASSERT_EQ(serial_number, std::stoi(TEST_VOL_SERIAL))
        << "Received: " << serial_number << std::endl;

    // Consider adding specific flags 
    // and check for them
    // ASSERT_EQ(file_system_flags, 271);
}

TEST_F(DokanTests, test_get_free_space) {
    std::error_code ec;
    const std::filesystem::space_info si = 
        std::filesystem::space(DEFAULT_MOUNTPOINT, ec);
    ASSERT_EQ(ec.value(), 0);

    ASSERT_NE(static_cast<std::intmax_t>(si.capacity), 0);
    ASSERT_NE(static_cast<std::intmax_t>(si.free), 0);
    ASSERT_NE(static_cast<std::intmax_t>(si.available), 0);
}

TEST_F(DokanTests, test_file_timestamp) {
    std::string file1 = DEFAULT_MOUNTPOINT"test_time1_"
                            + get_uuid();
    std::string file2 = DEFAULT_MOUNTPOINT"test_time2_"
                            + get_uuid();
    std::string file3 = DEFAULT_MOUNTPOINT"test_time3_"
                            + get_uuid();

    std::ofstream{file1};
    Sleep(1000);
    std::ofstream{file2};
    Sleep(1000);
    std::ofstream{file3};

    int64_t file1_creation = fs::last_write_time(file1)
                                 .time_since_epoch().count();
    int64_t file2_creation = fs::last_write_time(file2)
                                 .time_since_epoch().count();
    int64_t file3_creation = fs::last_write_time(file3)
                                 .time_since_epoch().count();

    EXPECT_LT(file1_creation, file2_creation);
    EXPECT_LT(file2_creation, file3_creation);

    // add 1h to file 1 creation time
    fs::file_time_type file1_time = fs::last_write_time(file1);

    fs::last_write_time(file1, file1_time + 1h);

    int64_t file1_new_time = fs::last_write_time(file1)
                                 .time_since_epoch().count();

    EXPECT_EQ((file1_time + 1h).time_since_epoch().count(),
              file1_new_time);
    EXPECT_GT(file1_new_time, file2_creation);
    EXPECT_GT(file1_new_time, file3_creation);

    ASSERT_TRUE(fs::remove(file1));
    ASSERT_TRUE(fs::remove(file2));
    ASSERT_TRUE(fs::remove(file3));
}

TEST_F(DokanTests, test_delete_disposition) {
    std::string file_path = DEFAULT_MOUNTPOINT"test_disp_"
                            + get_uuid();
    HANDLE hFile = CreateFile(file_path.c_str(), 
                               GENERIC_ALL, // required for delete
                               0, // exclusive access
                               NULL, 
                               CREATE_ALWAYS,
                               0, 
                               NULL);
    
    ASSERT_NE(hFile, INVALID_HANDLE_VALUE)
        << "Could not open file: " << file_path
        << "err: " << GetLastError() << std::endl;
  
    FILE_DISPOSITION_INFO fdi;
    fdi.DeleteFile = TRUE; // marking for deletion

    ASSERT_NE(
        SetFileInformationByHandle(
            hFile,
            FileDispositionInfo,
            &fdi,
            sizeof(FILE_DISPOSITION_INFO)), 0);

    ASSERT_NE(CloseHandle(hFile), 0);
    ASSERT_FALSE(fs::exists(file_path));    
}

bool check_create_disposition(std::string path, DWORD disposition) {
    HANDLE hFile = CreateFile(path.c_str(), 
                              GENERIC_WRITE,
                              0, // exclusive access
                              NULL,
                              disposition,
                              0, 
                              NULL);

    if(hFile == INVALID_HANDLE_VALUE) {
        return false;
    }

    if(CloseHandle(hFile) == 0) {
        return false;
    }

    return true;
}

TEST_F(DokanTests, test_create_dispositions) {
    std::string file_path = DEFAULT_MOUNTPOINT"test_create_"
                            + get_uuid();
    std::string non_existant_file = DEFAULT_MOUNTPOINT
                                    "test_create_" + get_uuid();
  
    EXPECT_TRUE(
        check_create_disposition(file_path, CREATE_NEW));
    
    // CREATE_ALWAYS with existing file
    EXPECT_TRUE(
        check_create_disposition(file_path, CREATE_ALWAYS));
    EXPECT_EQ(GetLastError(), ERROR_ALREADY_EXISTS);

    // CREATE_NEW with existing file
    EXPECT_FALSE(
        check_create_disposition(file_path, CREATE_NEW));
    EXPECT_EQ(GetLastError(), ERROR_FILE_EXISTS);

    // OPEN_EXISTING with existing file
    EXPECT_TRUE(
        check_create_disposition(file_path, OPEN_EXISTING));

    ASSERT_FALSE(fs::exists(non_existant_file));
    // OPEN_EXISTING with non-existant file
    EXPECT_FALSE(
        check_create_disposition(non_existant_file, OPEN_EXISTING));
    EXPECT_EQ(GetLastError(), ERROR_FILE_NOT_FOUND);

    // OPEN_ALWAYS with existing file
    EXPECT_TRUE(
        check_create_disposition(file_path, OPEN_ALWAYS));
    EXPECT_EQ(GetLastError(), ERROR_ALREADY_EXISTS);

    ASSERT_FALSE(fs::exists(non_existant_file));
    // OPEN_ALWAYS with non-existant file
    EXPECT_TRUE(
        check_create_disposition(non_existant_file, OPEN_ALWAYS));
    EXPECT_EQ(GetLastError(), 0);
    
    ASSERT_TRUE(fs::remove(non_existant_file));

    // TRUNCATE_EXISTING with existing file
    EXPECT_TRUE(
        check_create_disposition(file_path, TRUNCATE_EXISTING));

    // TRUNCATE_EXISTING with non-existant file
    EXPECT_FALSE(
        check_create_disposition(non_existant_file,
                                 TRUNCATE_EXISTING));
    EXPECT_EQ(GetLastError(), ERROR_FILE_NOT_FOUND);

    // clean-up
    ASSERT_TRUE(fs::remove(file_path));
}
