/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#include <aws/common/allocator.h>
#include <aws/common/device_random.h>
#include <aws/common/file.h>
#include <aws/common/string.h>
#include <aws/common/system_info.h>

#include <aws/testing/aws_test_harness.h>

static int s_aws_fopen_test_helper(char *file_path, char *content) {
    char read_result[100];
    AWS_ZERO_ARRAY(read_result);
    FILE *file = aws_fopen(file_path, "w+");
    ASSERT_NOT_NULL(file);
    fprintf(file, "%s", content);
    fclose(file);
    FILE *readfile = aws_fopen(file_path, "r");
    ASSERT_NOT_NULL(readfile);
    size_t read_len = fread(read_result, sizeof(char), strlen(content), readfile);
    ASSERT_UINT_EQUALS(strlen(content), read_len);
    fclose(readfile);
    ASSERT_SUCCESS(strcmp(content, read_result));

#ifdef _WIN32
    wchar_t w_file_path[1000];
    /* plus one for the EOS */
    size_t file_path_len = strlen(file_path) + 1;
    MultiByteToWideChar(CP_UTF8, 0, file_path, -1, w_file_path, (int)file_path_len);
    ASSERT_SUCCESS(_wremove(w_file_path));
#else
    ASSERT_SUCCESS(remove(file_path));
#endif
    return AWS_OP_SUCCESS;
}

static int s_aws_fopen_content_matches(char *file_path, char *content) {
    char read_result[100];
    AWS_ZERO_ARRAY(read_result);
    FILE *file = aws_fopen(file_path, "rb");
    ASSERT_NOT_NULL(file);
    size_t read_len = fread(read_result, sizeof(char), strlen(content), file);
    ASSERT_UINT_EQUALS(strlen(content), read_len);
    fclose(file);
    ASSERT_SUCCESS(strcmp(content, read_result));

    return AWS_OP_SUCCESS;
}

static int s_aws_fopen_non_ascii_read_existing_file_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)allocator;
    (void)ctx;

    char expected_content[] = "This is a non-ascii file path file.";
    char file_path[] = "Å Éxample.txt";
    char read_result[100];
    AWS_ZERO_ARRAY(read_result);
    FILE *readfile = aws_fopen(file_path, "r");
    ASSERT_NOT_NULL(readfile);
    size_t read_len = fread(read_result, sizeof(char), strlen(expected_content), readfile);
    ASSERT_UINT_EQUALS(strlen(expected_content), read_len);
    fclose(readfile);
    ASSERT_SUCCESS(strcmp(expected_content, read_result));
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(aws_fopen_non_ascii_read_existing_file_test, s_aws_fopen_non_ascii_read_existing_file_test_fn)

static int s_aws_fopen_non_ascii_test_fn(struct aws_allocator *allocator, void *ctx) {

    (void)allocator;
    (void)ctx;
    char file_path[] = "Éxample.txt";
    char content[] = "samples";
    ASSERT_SUCCESS(s_aws_fopen_test_helper(file_path, content));
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(aws_fopen_non_ascii_test, s_aws_fopen_non_ascii_test_fn)

static int s_aws_fopen_ascii_test_fn(struct aws_allocator *allocator, void *ctx) {

    (void)allocator;
    (void)ctx;
    char file_path[] = "sample.txt";
    char content[] = "samples";
    ASSERT_SUCCESS(s_aws_fopen_test_helper(file_path, content));
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(aws_fopen_ascii_test, s_aws_fopen_ascii_test_fn)

struct directory_traversal_test_data {
    bool child_dir_verified;
    bool child_file_verified;
    bool root_file_verified;
};

static const char *s_first_child_dir_path = "dir_traversal_test" AWS_PATH_DELIM_STR "first_child_dir";

static const char *s_first_child_file_path =
    "dir_traversal_test" AWS_PATH_DELIM_STR "first_child_dir" AWS_PATH_DELIM_STR "child.txt";

static const char *s_root_child_path = "dir_traversal_test" AWS_PATH_DELIM_STR "root_child.txt";

bool s_on_directory_entry(const struct aws_directory_entry *entry, void *user_data) {
    struct directory_traversal_test_data *test_data = user_data;

    if (aws_byte_cursor_eq_c_str(&entry->relative_path, s_root_child_path)) {
        test_data->root_file_verified =
            entry->file_type & AWS_FILE_TYPE_FILE && entry->file_size &&
            s_aws_fopen_content_matches((char *)entry->relative_path.ptr, "dir_traversal_test->root_child.txt") ==
                AWS_OP_SUCCESS;
        return true;
    }

    if (aws_byte_cursor_eq_c_str(&entry->relative_path, s_first_child_file_path)) {
        test_data->child_file_verified =
            entry->file_type & AWS_FILE_TYPE_FILE && entry->file_size &&
            s_aws_fopen_content_matches(
                (char *)entry->relative_path.ptr, "dir_traversal_test->first_child_dir->child.txt") == AWS_OP_SUCCESS;
        return true;
    }

    if (aws_byte_cursor_eq_c_str(&entry->relative_path, s_first_child_dir_path)) {
        test_data->child_dir_verified = entry->file_type & AWS_FILE_TYPE_DIRECTORY;
        return true;
    }

    return false;
}

static int s_directory_traversal_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "dir_traversal_test");
    struct directory_traversal_test_data test_data;
    AWS_ZERO_STRUCT(test_data);

    ASSERT_SUCCESS(aws_directory_traverse(allocator, path, true, s_on_directory_entry, &test_data));
    ASSERT_TRUE(test_data.child_dir_verified);
    ASSERT_TRUE(test_data.root_file_verified);
    ASSERT_TRUE(test_data.child_file_verified);

    AWS_ZERO_STRUCT(test_data);
    ASSERT_SUCCESS(aws_directory_traverse(allocator, path, false, s_on_directory_entry, &test_data));
    ASSERT_TRUE(test_data.child_dir_verified);
    ASSERT_TRUE(test_data.root_file_verified);
    ASSERT_FALSE(test_data.child_file_verified);

    aws_string_destroy(path);

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_traversal_test, s_directory_traversal_test_fn)

static int s_directory_iteration_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "dir_traversal_test");

    struct aws_directory_iterator *iterator = aws_directory_entry_iterator_new(allocator, path);
    ASSERT_NOT_NULL(iterator);
    const struct aws_directory_entry *first_entry = aws_directory_entry_iterator_get_value(iterator);
    ASSERT_NOT_NULL(first_entry);

    bool first_child_dir_found = false;
    bool root_file_found = false;

    do {
        const struct aws_directory_entry *entry = aws_directory_entry_iterator_get_value(iterator);
        if (entry->file_type == AWS_FILE_TYPE_DIRECTORY) {
            struct aws_byte_cursor first_child_dir_path_cur = aws_byte_cursor_from_c_str(s_first_child_dir_path);
            ASSERT_BIN_ARRAYS_EQUALS(
                first_child_dir_path_cur.ptr,
                first_child_dir_path_cur.len,
                entry->relative_path.ptr,
                entry->relative_path.len);
            first_child_dir_found = true;

            struct aws_string *next_path = aws_string_new_from_cursor(allocator, &entry->relative_path);
            struct aws_directory_iterator *next_iter = aws_directory_entry_iterator_new(allocator, next_path);
            aws_string_destroy(next_path);
            ASSERT_NOT_NULL(next_iter);

            entry = aws_directory_entry_iterator_get_value(next_iter);
            struct aws_byte_cursor first_child_file_path_cur = aws_byte_cursor_from_c_str(s_first_child_file_path);
            ASSERT_BIN_ARRAYS_EQUALS(
                first_child_file_path_cur.ptr,
                first_child_file_path_cur.len,
                entry->relative_path.ptr,
                entry->relative_path.len);
            ASSERT_INT_EQUALS(AWS_FILE_TYPE_FILE, entry->file_type);

            ASSERT_ERROR(AWS_ERROR_LIST_EMPTY, aws_directory_entry_iterator_next(next_iter));
            aws_directory_entry_iterator_destroy(next_iter);
        } else {
            struct aws_byte_cursor root_child_file_path_cur = aws_byte_cursor_from_c_str(s_root_child_path);
            ASSERT_BIN_ARRAYS_EQUALS(
                root_child_file_path_cur.ptr,
                root_child_file_path_cur.len,
                entry->relative_path.ptr,
                entry->relative_path.len);
            ASSERT_INT_EQUALS(AWS_FILE_TYPE_FILE, entry->file_type);
            root_file_found = true;
        }
    } while (aws_directory_entry_iterator_next(iterator) == AWS_OP_SUCCESS);

    ASSERT_ERROR(AWS_ERROR_LIST_EMPTY, aws_directory_entry_iterator_next(iterator));
    ASSERT_SUCCESS(aws_directory_entry_iterator_previous(iterator));
    ASSERT_PTR_EQUALS(first_entry, aws_directory_entry_iterator_get_value(iterator));
    aws_directory_entry_iterator_destroy(iterator);
    aws_string_destroy(path);

    ASSERT_TRUE(root_file_found);
    ASSERT_TRUE(first_child_dir_found);

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_iteration_test, s_directory_iteration_test_fn)

static int s_directory_iteration_non_existent_directory_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "dir_traversal_test_non_existent");

    struct aws_directory_iterator *iterator = aws_directory_entry_iterator_new(allocator, path);
    ASSERT_NULL(iterator);
    ASSERT_INT_EQUALS(aws_last_error(), AWS_ERROR_FILE_INVALID_PATH);

    aws_string_destroy(path);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_iteration_non_existent_directory_test, s_directory_iteration_non_existent_directory_test_fn)

struct directory_traversal_abort_test_data {
    int times_called;
};

bool directory_traversal_abort_test_data(const struct aws_directory_entry *entry, void *user_data) {
    (void)entry;
    struct directory_traversal_abort_test_data *test_data = user_data;
    test_data->times_called += 1;

    return false;
}

static int s_directory_traversal_stop_traversal_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "dir_traversal_test");
    struct directory_traversal_abort_test_data test_data;
    AWS_ZERO_STRUCT(test_data);

    ASSERT_ERROR(
        AWS_ERROR_OPERATION_INTERUPTED,
        aws_directory_traverse(allocator, path, true, directory_traversal_abort_test_data, &test_data));
    ASSERT_INT_EQUALS(1, test_data.times_called);

    aws_string_destroy(path);

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_traversal_stop_traversal, s_directory_traversal_stop_traversal_fn)

static int s_directory_traversal_on_file_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "dir_traversal_test/root_child.txt");
    struct directory_traversal_test_data test_data;
    AWS_ZERO_STRUCT(test_data);

    ASSERT_ERROR(
        AWS_ERROR_FILE_INVALID_PATH, aws_directory_traverse(allocator, path, true, s_on_directory_entry, &test_data));

    aws_string_destroy(path);

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_traversal_on_file_test, s_directory_traversal_on_file_test_fn)

static int s_directory_existence_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "dir_traversal_test");
    ASSERT_TRUE(aws_directory_exists(path));
    aws_string_destroy(path);

    path = aws_string_new_from_c_str(allocator, "dir_traversal_test_blah");
    ASSERT_FALSE(aws_directory_exists(path));
    aws_string_destroy(path);

    path = aws_string_new_from_c_str(allocator, "dir_traversal_test/root_child.txt");
    ASSERT_FALSE(aws_directory_exists(path));
    aws_string_destroy(path);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_existence_test, s_directory_existence_test_fn)

static int s_directory_creation_deletion_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "temp_dir");
    ASSERT_SUCCESS(aws_directory_create(path));

    /* should be idempotent */
    ASSERT_SUCCESS(aws_directory_create(path));

    ASSERT_TRUE(aws_directory_exists(path));
    ASSERT_SUCCESS(aws_directory_delete(path, false));
    ASSERT_FALSE(aws_directory_exists(path));

    aws_string_destroy(path);

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_creation_deletion_test, s_directory_creation_deletion_test_fn)

static int s_directory_non_empty_deletion_fails_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "dir_traversal_test");
    ASSERT_TRUE(aws_directory_exists(path));
    ASSERT_ERROR(AWS_ERROR_DIRECTORY_NOT_EMPTY, aws_directory_delete(path, false));
    ASSERT_TRUE(aws_directory_exists(path));

    aws_string_destroy(path);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_non_empty_deletion_fails_test, s_directory_non_empty_deletion_fails_test_fn)

static int s_directory_non_empty_deletion_recursively_succeeds_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "non_empty_dir_del_test_dir_1");
    ASSERT_SUCCESS(aws_directory_create(path));

    const char *nested_dir = "non_empty_dir_del_test_dir_1" AWS_PATH_DELIM_STR "test_dir_2";
    struct aws_string *nested_dir_path = aws_string_new_from_c_str(allocator, nested_dir);
    ASSERT_SUCCESS(aws_directory_create(nested_dir_path));

    const char *nested_file =
        "non_empty_dir_del_test_dir_1" AWS_PATH_DELIM_STR "test_dir_2" AWS_PATH_DELIM_STR "nested_file.txt";

    FILE *nested_file_ptr = aws_fopen(nested_file, "w");
    ASSERT_NOT_NULL(nested_file_ptr);
    fclose(nested_file_ptr);

    ASSERT_SUCCESS(aws_directory_delete(path, true));
    ASSERT_FALSE(aws_directory_exists(path));

    aws_string_destroy(nested_dir_path);
    aws_string_destroy(path);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(
    directory_non_empty_deletion_recursively_succeeds_test,
    s_directory_non_empty_deletion_recursively_succeeds_test_fn)

static int s_directory_move_succeeds_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "directory_move_succeeds_test_dir_1");
    ASSERT_SUCCESS(aws_directory_create(path));

    struct aws_string *to_path = aws_string_new_from_c_str(allocator, "directory_move_succeeds_test_dir_2");
    ASSERT_SUCCESS(aws_directory_or_file_move(path, to_path));

    ASSERT_FALSE(aws_directory_exists(path));
    ASSERT_TRUE(aws_directory_exists(to_path));

    ASSERT_SUCCESS(aws_directory_delete(to_path, true));

    aws_string_destroy(to_path);
    aws_string_destroy(path);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_move_succeeds_test, s_directory_move_succeeds_test_fn)

static int s_directory_move_src_non_existent_test_fn(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;
    struct aws_string *path = aws_string_new_from_c_str(allocator, "directory_move_src_non_existent_test_dir_1");

    struct aws_string *to_path = aws_string_new_from_c_str(allocator, "directory_move_src_non_existent_test_dir_2");
    ASSERT_ERROR(AWS_ERROR_FILE_INVALID_PATH, aws_directory_or_file_move(path, to_path));

    aws_string_destroy(to_path);
    aws_string_destroy(path);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(directory_move_src_non_existent_test, s_directory_move_src_non_existent_test_fn)

static int s_test_home_directory_not_null(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_string *home_directory = aws_get_home_directory(allocator);
    ASSERT_TRUE(home_directory != NULL);

    aws_string_destroy(home_directory);

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(test_home_directory_not_null, s_test_home_directory_not_null);

static int s_test_normalize_posix_directory_separator(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_string *buffer = aws_string_new_from_c_str(allocator, "./test/path/abc");
    struct aws_byte_buf path_buf = aws_byte_buf_from_array(buffer->bytes, buffer->len);
    aws_normalize_directory_separator(&path_buf);
    for (size_t i = 0; i < path_buf.len; ++i) {
        if (aws_is_any_directory_separator((char)path_buf.buffer[i])) {
            ASSERT_INT_EQUALS(aws_get_platform_directory_separator(), path_buf.buffer[i]);
        }
    }

    aws_string_destroy(buffer);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(test_normalize_posix_directory_separator, s_test_normalize_posix_directory_separator);

static int s_test_normalize_windows_directory_separator(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_string *buffer = aws_string_new_from_c_str(allocator, ".\\test\\path\\abc");
    struct aws_byte_buf path_buf = aws_byte_buf_from_array(buffer->bytes, buffer->len);
    aws_normalize_directory_separator(&path_buf);
    for (size_t i = 0; i < path_buf.len; ++i) {
        if (aws_is_any_directory_separator((char)path_buf.buffer[i])) {
            ASSERT_INT_EQUALS(aws_get_platform_directory_separator(), path_buf.buffer[i]);
        }
    }
    aws_string_destroy(buffer);
    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(test_normalize_windows_directory_separator, s_test_normalize_windows_directory_separator);

static int s_check_byte_buf_from_file(const struct aws_byte_buf *buf, struct aws_byte_cursor expected_contents) {
    ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&expected_contents, buf), "Contents should match");
    ASSERT_TRUE(buf->capacity > buf->len, "Buffer should end with null-terminator");
    ASSERT_UINT_EQUALS(0, buf->buffer[buf->len], "Buffer should end with null-terminator");
    return AWS_OP_SUCCESS;
}

static int s_create_file_then_read_it(struct aws_allocator *allocator, struct aws_byte_cursor contents) {
    /* create file */
    const char *filename = "testy";
    FILE *f = aws_fopen(filename, "wb");
    ASSERT_UINT_EQUALS(contents.len, fwrite(contents.ptr, 1, contents.len, f));
    ASSERT_INT_EQUALS(0, fclose(f));

    struct aws_byte_buf buf;

    /* check aws_byte_buf_init_from_file() */
    ASSERT_SUCCESS(aws_byte_buf_init_from_file(&buf, allocator, filename));
    ASSERT_SUCCESS(s_check_byte_buf_from_file(&buf, contents));
    aws_byte_buf_clean_up(&buf);

    /* now check aws_byte_buf_init_from_file_with_size_hint() ... */

    /* size_hint more then big enough */
    size_t size_hint = contents.len * 2;
    ASSERT_SUCCESS(aws_byte_buf_init_from_file_with_size_hint(&buf, allocator, filename, size_hint));
    ASSERT_SUCCESS(s_check_byte_buf_from_file(&buf, contents));
    aws_byte_buf_clean_up(&buf);

    /* size_hint not big enough for null-terminator */
    size_hint = contents.len;
    ASSERT_SUCCESS(aws_byte_buf_init_from_file_with_size_hint(&buf, allocator, filename, size_hint));
    ASSERT_SUCCESS(s_check_byte_buf_from_file(&buf, contents));
    aws_byte_buf_clean_up(&buf);

    /* size_hint 0 */
    size_hint = 0;
    ASSERT_SUCCESS(aws_byte_buf_init_from_file_with_size_hint(&buf, allocator, filename, size_hint));
    ASSERT_SUCCESS(s_check_byte_buf_from_file(&buf, contents));
    aws_byte_buf_clean_up(&buf);

    /* size_hint 1 */
    size_hint = 1;
    ASSERT_SUCCESS(aws_byte_buf_init_from_file_with_size_hint(&buf, allocator, filename, size_hint));
    ASSERT_SUCCESS(s_check_byte_buf_from_file(&buf, contents));
    aws_byte_buf_clean_up(&buf);

    remove(filename);
    return AWS_OP_SUCCESS;
}

/* Read an actual "special file" (if it exists on this machine) */
static int s_read_special_file(struct aws_allocator *allocator, const char *filename) {
    struct aws_string *filename_str = aws_string_new_from_c_str(allocator, filename);
    bool exists = aws_path_exists(filename_str);
    aws_string_destroy(filename_str);
    if (!exists) {
        return AWS_OP_SUCCESS;
    }

    struct aws_byte_buf buf;
    ASSERT_SUCCESS(aws_byte_buf_init_from_file(&buf, allocator, filename));
    ASSERT_TRUE(buf.capacity > buf.len, "Buffer should end with null-terminator");
    ASSERT_UINT_EQUALS(0, buf.buffer[buf.len], "Buffer should end with null-terminator");

    if (strcmp("/dev/null", filename) == 0) {
        ASSERT_UINT_EQUALS(0, buf.len, "expected /dev/null to be empty");
    } else {
        ASSERT_TRUE(buf.len > 0, "expected special file to have data");
    }

    aws_byte_buf_clean_up(&buf);
    return AWS_OP_SUCCESS;
}

static int s_test_byte_buf_init_from_file(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    /* simple text file */
    ASSERT_SUCCESS(s_create_file_then_read_it(allocator, aws_byte_cursor_from_c_str("asdf")));

    /* empty file */
    ASSERT_SUCCESS(s_create_file_then_read_it(allocator, aws_byte_cursor_from_c_str("")));

    /* large 3MB+1byte binary file */
    struct aws_byte_buf big_rando;
    aws_byte_buf_init(&big_rando, allocator, (1024 * 1024 * 3) + 1);
    ASSERT_SUCCESS(aws_device_random_buffer(&big_rando));
    ASSERT_SUCCESS(s_create_file_then_read_it(allocator, aws_byte_cursor_from_buf(&big_rando)));
    aws_byte_buf_clean_up(&big_rando);

    /* test some "special files" (if they exist) */
    ASSERT_SUCCESS(s_read_special_file(allocator, "/proc/cpuinfo"));
    ASSERT_SUCCESS(s_read_special_file(allocator, "/proc/net/tcp"));
    ASSERT_SUCCESS(s_read_special_file(allocator, "/sys/devices/virtual/dmi/id/sys_vendor"));
    ASSERT_SUCCESS(s_read_special_file(allocator, "/dev/null"));

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(test_byte_buf_init_from_file, s_test_byte_buf_init_from_file)

struct aws_file_path_read_from_offset_tester {
    struct aws_allocator *alloc;
    struct aws_allocator *aligned_allocator;
    size_t page_size;
    struct aws_string *file_path;
    struct aws_byte_buf content;
    size_t file_length;
};

static int s_file_path_read_from_offset_tester_init(
    struct aws_file_path_read_from_offset_tester *tester,
    struct aws_allocator *allocator,
    char *file_path,
    size_t file_length) {

    tester->alloc = allocator;
    tester->page_size = aws_system_info_page_size();
    tester->aligned_allocator = aws_explicit_aligned_allocator_new(tester->page_size);
    if (!tester->aligned_allocator) {
        return AWS_OP_ERR;
    }
    tester->file_path = aws_string_new_from_c_str(tester->aligned_allocator, file_path);
    tester->file_length = file_length;
    struct aws_byte_cursor content = aws_byte_cursor_from_c_str("0123456789abcdef");
    aws_byte_buf_init_copy_from_cursor(&tester->content, allocator, content);

    FILE *file = aws_fopen(file_path, "w+");
    ASSERT_NOT_NULL(file);

    /* Write to the file, repeating the content until the file length is met */
    size_t bytes_written = 0;
    while (bytes_written < file_length) {
        size_t bytes_to_write = aws_min_size(content.len, file_length - bytes_written);
        size_t written = fwrite(content.ptr, 1, bytes_to_write, file);
        if (written != bytes_to_write) {
            fclose(file);
            return AWS_OP_ERR;
        }
        bytes_written += written;
    }

    fclose(file);
    return AWS_OP_SUCCESS;
}

static void s_file_path_read_from_offset_tester_cleanup(struct aws_file_path_read_from_offset_tester *tester) {
    aws_byte_buf_clean_up(&tester->content);
    remove(aws_string_c_str(tester->file_path));
    aws_string_destroy(tester->file_path);
    aws_explicit_aligned_allocator_destroy(tester->aligned_allocator);
}

static int s_test_file_path_read_from_offset_direct_io(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

#if defined(AWS_OS_LINUX)
    struct aws_file_path_read_from_offset_tester tester;
    char file_path[] = "test_file_path_read_from_offset_direct_io.txt";

    /* Create a file that's at least 2 pages in size to test offset reading */
    size_t page_size = aws_system_info_page_size();
    size_t file_size = page_size * 2;

    ASSERT_SUCCESS(s_file_path_read_from_offset_tester_init(&tester, allocator, file_path, file_size));

    /* Test 1: Read the first page and check the result matches expectation */
    struct aws_byte_buf output_buf;
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, tester.aligned_allocator, page_size));

    size_t actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset_direct_io(tester.file_path, 0, page_size, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(page_size, actual_read);
    ASSERT_UINT_EQUALS(page_size, output_buf.len);

    /* Verify the content matches what we expect from the first page */
    struct aws_byte_cursor expected_content = aws_byte_cursor_from_c_str("0123456789abcdef");
    for (size_t i = 0; i < page_size; i++) {
        size_t pattern_index = i % expected_content.len;
        ASSERT_UINT_EQUALS(expected_content.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Test 2: Set offset to the page size, and read the next page */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, tester.aligned_allocator, page_size));

    actual_read = 0;
    ASSERT_SUCCESS(
        aws_file_path_read_from_offset_direct_io(tester.file_path, page_size, page_size, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(page_size, actual_read);
    ASSERT_UINT_EQUALS(page_size, output_buf.len);

    /* Verify the content matches what we expect from the second page */
    for (size_t i = 0; i < page_size; i++) {
        size_t file_position = page_size + i;
        size_t pattern_index = file_position % expected_content.len;
        ASSERT_UINT_EQUALS(expected_content.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Test 3: Test unaligned offset - should fail with AWS_ERROR_INVALID_ARGUMENT */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, tester.aligned_allocator, page_size));

    size_t unaligned_offset = 1; /* Not aligned to page boundary */
    ASSERT_FAILS(aws_file_path_read_from_offset_direct_io(
        tester.file_path, unaligned_offset, page_size, &output_buf, &actual_read));
    ASSERT_UINT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, aws_last_error());

    aws_byte_buf_clean_up(&output_buf);

    /* Test 4: Test unaligned size - should fail with AWS_ERROR_INVALID_ARGUMENT */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, tester.aligned_allocator, page_size));

    size_t unaligned_size = page_size - 1; /* Not aligned to page boundary */
    ASSERT_FAILS(
        aws_file_path_read_from_offset_direct_io(tester.file_path, 0, unaligned_size, &output_buf, &actual_read));
    ASSERT_UINT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, aws_last_error());

    aws_byte_buf_clean_up(&output_buf);

    /* Cleanup */
    s_file_path_read_from_offset_tester_cleanup(&tester);
#else
    struct aws_string *file_path =
        aws_string_new_from_c_str(allocator, "test_file_path_read_from_offset_direct_io.txt");
    ASSERT_FAILS(aws_file_path_read_from_offset_direct_io(file_path, 0, 10, NULL, NULL));
    ASSERT_UINT_EQUALS(AWS_ERROR_UNSUPPORTED_OPERATION, aws_last_error());
    aws_string_destroy(file_path);
#endif

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(test_file_path_read_from_offset_direct_io, s_test_file_path_read_from_offset_direct_io)

static int s_test_file_path_read_from_offset(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

    struct aws_file_path_read_from_offset_tester tester;
    char file_path[] = "test_file_path_read_from_offset.txt";

    /* Create a test file with known content */
    size_t file_size = 1024; /* 1KB file for testing */

    ASSERT_SUCCESS(s_file_path_read_from_offset_tester_init(&tester, allocator, file_path, file_size));

    /* Test 1: Read from the beginning of the file */
    struct aws_byte_buf output_buf;
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, allocator, 100));

    size_t actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset(tester.file_path, 0, 100, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(100, actual_read);
    ASSERT_UINT_EQUALS(100, output_buf.len);

    /* Verify the content matches what we expect from the beginning */
    struct aws_byte_cursor expected_content = aws_byte_cursor_from_c_str("0123456789abcdef");
    for (size_t i = 0; i < 100; i++) {
        size_t pattern_index = i % expected_content.len;
        ASSERT_UINT_EQUALS(expected_content.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Test 2: Read from an offset in the middle of the file */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, allocator, 50));

    size_t offset = 200;
    actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset(tester.file_path, offset, 50, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(50, actual_read);
    ASSERT_UINT_EQUALS(50, output_buf.len);

    /* Verify the content matches what we expect from the offset position */
    for (size_t i = 0; i < 50; i++) {
        size_t file_position = offset + i;
        size_t pattern_index = file_position % expected_content.len;
        ASSERT_UINT_EQUALS(expected_content.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Test 3: Read near the end of the file */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, allocator, 100));

    offset = file_size - 50; /* Read the last 50 bytes */
    actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset(tester.file_path, offset, 100, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(50, actual_read); /* Should only read 50 bytes since that's all that's left */
    ASSERT_UINT_EQUALS(50, output_buf.len);

    /* Verify the content matches what we expect from near the end */
    for (size_t i = 0; i < 50; i++) {
        size_t file_position = offset + i;
        size_t pattern_index = file_position % expected_content.len;
        ASSERT_UINT_EQUALS(expected_content.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Test 4: Try to read beyond the end of the file */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, allocator, 100));

    offset = file_size + 10; /* Beyond the end of the file */
    actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset(tester.file_path, offset, 100, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(0, actual_read); /* Should read 0 bytes */
    ASSERT_UINT_EQUALS(0, output_buf.len);

    aws_byte_buf_clean_up(&output_buf);

    /* Test 5: Read with unaligned offset and size (should work fine for regular read) */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, allocator, 37));

    offset = 13; /* Arbitrary unaligned offset */
    actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset(tester.file_path, offset, 37, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(37, actual_read);
    ASSERT_UINT_EQUALS(37, output_buf.len);

    /* Verify the content matches what we expect from the unaligned offset */
    for (size_t i = 0; i < 37; i++) {
        size_t file_position = offset + i;
        size_t pattern_index = file_position % expected_content.len;
        ASSERT_UINT_EQUALS(expected_content.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Test 6: Test with zero-length read */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, allocator, 0));

    actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset(tester.file_path, 0, 0, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(0, actual_read);
    ASSERT_UINT_EQUALS(0, output_buf.len);

    aws_byte_buf_clean_up(&output_buf);

    /* Cleanup */
    s_file_path_read_from_offset_tester_cleanup(&tester);

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(test_file_path_read_from_offset, s_test_file_path_read_from_offset)

static int s_test_file_path_read_from_offset_direct_io_chunking(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

#if defined(AWS_OS_LINUX)
    struct aws_file_path_read_from_offset_tester tester;
    char file_path[] = "test_direct_io_chunking.txt";
    /* Instead of creating a 2GiB file to test, we use the separate API that allows us to pass in the chunk size. */
    size_t chunk_size = 8192;
    size_t page_size = aws_system_info_page_size();
    size_t file_size = chunk_size * 2 + page_size; /* Ensure it's larger than chunk size */

    ASSERT_SUCCESS(s_file_path_read_from_offset_tester_init(&tester, allocator, file_path, file_size));

    /* Test 1: Read exactly chunk_size bytes - should not trigger chunking */
    struct aws_byte_buf output_buf;
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, tester.aligned_allocator, chunk_size));

    size_t actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset_direct_io_with_chunk_size(
        tester.file_path, 0, chunk_size, chunk_size, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(chunk_size, actual_read);
    ASSERT_UINT_EQUALS(chunk_size, output_buf.len);

    /* Verify the content matches our expected pattern */
    struct aws_byte_cursor expected_pattern = aws_byte_cursor_from_c_str("0123456789abcdef");
    for (size_t i = 0; i < chunk_size; i++) {
        size_t pattern_index = i % expected_pattern.len;
        ASSERT_UINT_EQUALS(expected_pattern.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Test 2: Read more than chunk_size bytes - should trigger chunking */
    size_t large_read_size = chunk_size + page_size; /* Ensure it's page-aligned */
    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, tester.aligned_allocator, large_read_size));

    actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset_direct_io_with_chunk_size(
        tester.file_path, 0, large_read_size, chunk_size, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(large_read_size, actual_read);
    ASSERT_UINT_EQUALS(large_read_size, output_buf.len);

    /* Verify the content matches our expected pattern across the entire read */
    for (size_t i = 0; i < large_read_size; i++) {
        size_t pattern_index = i % expected_pattern.len;
        ASSERT_UINT_EQUALS(expected_pattern.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Test 3: Read much more than chunk_size - multiple chunks */
    size_t very_large_read_size = file_size; /* Ensure it's page-aligned */

    ASSERT_SUCCESS(aws_byte_buf_init(&output_buf, tester.aligned_allocator, very_large_read_size));

    actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset_direct_io_with_chunk_size(
        tester.file_path, 0, very_large_read_size, chunk_size, &output_buf, &actual_read));

    ASSERT_UINT_EQUALS(very_large_read_size, actual_read);
    ASSERT_UINT_EQUALS(very_large_read_size, output_buf.len);

    /* Verify the content matches our expected pattern across the entire read */
    for (size_t i = 0; i < very_large_read_size; i++) {
        size_t pattern_index = i % expected_pattern.len;
        ASSERT_UINT_EQUALS(expected_pattern.ptr[pattern_index], output_buf.buffer[i]);
    }

    aws_byte_buf_clean_up(&output_buf);

    /* Cleanup */
    s_file_path_read_from_offset_tester_cleanup(&tester);

#else
    /* On non-Linux platforms, the function should return AWS_ERROR_UNSUPPORTED_OPERATION */
    struct aws_string *file_path = aws_string_new_from_c_str(allocator, "test_direct_io_chunking.txt");
    struct aws_byte_buf dummy_buf;
    aws_byte_buf_init(&dummy_buf, allocator, 1024);
    size_t dummy_read = 0;

    ASSERT_FAILS(aws_file_path_read_from_offset_direct_io(file_path, 0, 1024, &dummy_buf, &dummy_read));
    ASSERT_UINT_EQUALS(AWS_ERROR_UNSUPPORTED_OPERATION, aws_last_error());

    aws_byte_buf_clean_up(&dummy_buf);
    aws_string_destroy(file_path);
#endif

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(test_file_path_read_from_offset_direct_io_chunking, s_test_file_path_read_from_offset_direct_io_chunking)

static int s_test_file_path_write_to_offset_direct_io(struct aws_allocator *allocator, void *ctx) {
    (void)ctx;

#if defined(AWS_OS_LINUX)
    size_t page_size = aws_system_info_page_size();
    struct aws_allocator *aligned_alloc = aws_explicit_aligned_allocator_new(page_size);
    ASSERT_NOT_NULL(aligned_alloc);

    char file_path_cstr[] = "test_file_path_write_to_offset_direct_io.txt";
    struct aws_string *file_path = aws_string_new_from_c_str(allocator, file_path_cstr);

    /* Create the file first (function requires it to exist) */
    FILE *f = aws_fopen(file_path_cstr, "wb");
    ASSERT_NOT_NULL(f);
    fclose(f);

    /* Test 1: Write 2 aligned pages of 'A' at offset 0 */
    size_t two_pages = page_size * 2;
    struct aws_byte_buf write_buf;
    ASSERT_SUCCESS(aws_byte_buf_init(&write_buf, aligned_alloc, two_pages));
    memset(write_buf.buffer, 'A', two_pages);
    write_buf.len = two_pages;
    struct aws_byte_cursor write_cursor = aws_byte_cursor_from_buf(&write_buf);
    ASSERT_SUCCESS(aws_file_path_write_to_offset_direct_io(file_path, 0, write_cursor));

    /* Read back and verify */
    struct aws_byte_buf read_buf;
    ASSERT_SUCCESS(aws_byte_buf_init(&read_buf, aligned_alloc, two_pages));
    size_t actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset_direct_io(file_path, 0, two_pages, &read_buf, &actual_read));
    ASSERT_UINT_EQUALS(two_pages, actual_read);
    for (size_t i = 0; i < two_pages; i++) {
        ASSERT_UINT_EQUALS('A', read_buf.buffer[i]);
    }
    aws_byte_buf_clean_up(&read_buf);

    /* Test 2: Write 1 page of 'C' at page-aligned offset (second page) */
    struct aws_byte_buf overwrite_buf;
    ASSERT_SUCCESS(aws_byte_buf_init(&overwrite_buf, aligned_alloc, page_size));
    memset(overwrite_buf.buffer, 'C', page_size);
    overwrite_buf.len = page_size;
    struct aws_byte_cursor overwrite_cursor = aws_byte_cursor_from_buf(&overwrite_buf);
    ASSERT_SUCCESS(aws_file_path_write_to_offset_direct_io(file_path, (uint64_t)page_size, overwrite_cursor));

    /* Read back and verify first page still 'A', second page now 'C' */
    ASSERT_SUCCESS(aws_byte_buf_init(&read_buf, aligned_alloc, two_pages));
    actual_read = 0;
    ASSERT_SUCCESS(aws_file_path_read_from_offset_direct_io(file_path, 0, two_pages, &read_buf, &actual_read));
    ASSERT_UINT_EQUALS(two_pages, actual_read);
    for (size_t i = 0; i < page_size; i++) {
        ASSERT_UINT_EQUALS('A', read_buf.buffer[i]);
    }
    for (size_t i = page_size; i < two_pages; i++) {
        ASSERT_UINT_EQUALS('C', read_buf.buffer[i]);
    }
    aws_byte_buf_clean_up(&read_buf);
    aws_byte_buf_clean_up(&overwrite_buf);

    /* Test 3: Unaligned MUST fail */
    struct aws_byte_buf unaligned_buf;
    size_t unaligned_len = page_size + 37;
    ASSERT_SUCCESS(aws_byte_buf_init(&unaligned_buf, aligned_alloc, unaligned_len));
    memset(unaligned_buf.buffer, 'X', unaligned_len);
    unaligned_buf.len = unaligned_len;
    struct aws_byte_cursor unaligned_cursor = aws_byte_cursor_from_buf(&unaligned_buf);
    ASSERT_FAILS(aws_file_path_write_to_offset_direct_io(file_path, 37, unaligned_cursor));
    ASSERT_UINT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, aws_last_error());
    aws_byte_buf_clean_up(&unaligned_buf);

    /* Test 4: File does not exist MUST fail */
    struct aws_string *nonexistent = aws_string_new_from_c_str(allocator, "nonexistent_direct_io_file.txt");
    struct aws_byte_cursor one_page_cursor = {.ptr = write_buf.buffer, .len = page_size};
    ASSERT_FAILS(aws_file_path_write_to_offset_direct_io(nonexistent, 0, one_page_cursor));
    aws_string_destroy(nonexistent);

    /* Test 5: Mixed O_DIRECT + buffered writes at non-overlapping offsets, verify all data correct.
     * Simulates the download pattern: O_DIRECT for aligned parts, buffered write for last part tail. */
    remove(file_path_cstr);
    f = aws_fopen(file_path_cstr, "wb");
    ASSERT_NOT_NULL(f);
    fclose(f);

    /* Write 3 pages with O_DIRECT: page 0='D', page 1='E', page 2='F' */
    memset(write_buf.buffer, 'D', page_size);
    write_cursor = (struct aws_byte_cursor){.ptr = write_buf.buffer, .len = page_size};
    ASSERT_SUCCESS(aws_file_path_write_to_offset_direct_io(file_path, 0, write_cursor));

    memset(write_buf.buffer, 'E', page_size);
    ASSERT_SUCCESS(aws_file_path_write_to_offset_direct_io(file_path, (uint64_t)page_size, write_cursor));

    memset(write_buf.buffer, 'F', page_size);
    ASSERT_SUCCESS(aws_file_path_write_to_offset_direct_io(file_path, (uint64_t)(page_size * 2), write_cursor));

    /* Write unaligned tail (37 bytes of 'G') at offset page_size*3 using buffered write */
    size_t tail_len = 37;
    uint8_t tail_data[37];
    memset(tail_data, 'G', tail_len);
    FILE *tail_file = aws_fopen(file_path_cstr, "r+b");
    ASSERT_NOT_NULL(tail_file);
    ASSERT_TRUE(fseek(tail_file, (long)(page_size * 3), SEEK_SET) == 0);
    size_t written = fwrite(tail_data, 1, tail_len, tail_file);
    ASSERT_INT_EQUALS((int)tail_len, (int)written);
    fclose(tail_file);

    /* Read entire file back and verify all regions */
    size_t total_size = page_size * 3 + tail_len;
    FILE *verify_file = aws_fopen(file_path_cstr, "rb");
    ASSERT_NOT_NULL(verify_file);
    uint8_t *verify_buf = aws_mem_calloc(allocator, 1, total_size);
    ASSERT_UINT_EQUALS(total_size, fread(verify_buf, 1, total_size, verify_file));
    fclose(verify_file);

    for (size_t i = 0; i < page_size; i++) {
        ASSERT_UINT_EQUALS('D', verify_buf[i]);
    }
    for (size_t i = page_size; i < page_size * 2; i++) {
        ASSERT_UINT_EQUALS('E', verify_buf[i]);
    }
    for (size_t i = page_size * 2; i < page_size * 3; i++) {
        ASSERT_UINT_EQUALS('F', verify_buf[i]);
    }
    for (size_t i = page_size * 3; i < total_size; i++) {
        ASSERT_UINT_EQUALS('G', verify_buf[i]);
    }
    aws_mem_release(allocator, verify_buf);

    /* Cleanup */
    aws_byte_buf_clean_up(&write_buf);
    remove(file_path_cstr);
    aws_string_destroy(file_path);
    aws_explicit_aligned_allocator_destroy(aligned_alloc);
#else
    (void)allocator;
    struct aws_string *file_path = aws_string_new_from_c_str(allocator, "dummy.txt");
    struct aws_byte_cursor empty = {.ptr = NULL, .len = 0};
    ASSERT_FAILS(aws_file_path_write_to_offset_direct_io(file_path, 0, empty));
    ASSERT_UINT_EQUALS(AWS_ERROR_UNSUPPORTED_OPERATION, aws_last_error());
    aws_string_destroy(file_path);
#endif

    return AWS_OP_SUCCESS;
}

AWS_TEST_CASE(test_file_path_write_to_offset_direct_io, s_test_file_path_write_to_offset_direct_io)
