#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>

#define ASSERT_TRUE(cond, fmt, ...)                                             \
    do {                                                                        \
        if (!(cond)) {                                                          \
            fprintf(stderr, "[FAIL] %s:%d " fmt "\n", __func__, __LINE__,      \
                    ##__VA_ARGS__);                                             \
            return -1;                                                          \
        }                                                                       \
    } while (0)

#define ASSERT_SYS_OK(expr)                                                     \
    do {                                                                        \
        if ((expr) < 0) {                                                       \
            fprintf(stderr, "[FAIL] %s:%d %s: %s\n", __func__, __LINE__,       \
                    #expr, strerror(errno));                                    \
            return -1;                                                          \
        }                                                                       \
    } while (0)

static int
join_path(char *out, size_t out_sz, const char *dir, const char *name)
{
    int n = snprintf(out, out_sz, "%s/%s", dir, name);
    if (n < 0 || (size_t)n >= out_sz) {
        errno = ENAMETOOLONG;
        return -1;
    }
    return 0;
}

static int
test_basic_rw_seek_stat(const char *workdir)
{
    char path[PATH_MAX];
    ASSERT_SYS_OK(join_path(path, sizeof(path), workdir, "basic_rw.txt"));

    int fd = open(path, O_CREAT | O_TRUNC | O_RDWR, 0644);
    ASSERT_SYS_OK(fd);

    const char *init = "abcdef";
    ssize_t nr = write(fd, init, 6);
    ASSERT_TRUE(nr == 6, "write expected 6, got %zd", nr);

    off_t off = lseek(fd, 0, SEEK_SET);
    ASSERT_TRUE(off == 0, "lseek expected 0, got %lld", (long long)off);

    char buf[16] = {0};
    nr = read(fd, buf, 6);
    ASSERT_TRUE(nr == 6, "read expected 6, got %zd", nr);
    ASSERT_TRUE(memcmp(buf, "abcdef", 6) == 0, "read content mismatch");

    nr = pwrite(fd, "XYZ", 3, 3);
    ASSERT_TRUE(nr == 3, "pwrite expected 3, got %zd", nr);

    memset(buf, 0, sizeof(buf));
    nr = pread(fd, buf, 6, 0);
    ASSERT_TRUE(nr == 6, "pread expected 6, got %zd", nr);
    ASSERT_TRUE(memcmp(buf, "abcXYZ", 6) == 0, "pread content mismatch");

    struct stat st;
    ASSERT_SYS_OK(fstat(fd, &st));
    ASSERT_TRUE(st.st_size == 6, "fstat size expected 6, got %lld",
                (long long)st.st_size);

    ASSERT_SYS_OK(ftruncate(fd, 4));
    ASSERT_SYS_OK(fstat(fd, &st));
    ASSERT_TRUE(st.st_size == 4, "ftruncate size expected 4, got %lld",
                (long long)st.st_size);

    ASSERT_SYS_OK(fdatasync(fd));
    ASSERT_SYS_OK(fsync(fd));
    ASSERT_SYS_OK(close(fd));
    ASSERT_SYS_OK(unlink(path));
    return 0;
}

static int
test_openat_rename_unlink(const char *workdir)
{
    char subdir[PATH_MAX];
    ASSERT_SYS_OK(join_path(subdir, sizeof(subdir), workdir, "openat_dir"));

    ASSERT_SYS_OK(mkdir(subdir, 0755));

    int dfd = open(subdir, O_RDONLY | O_DIRECTORY);
    ASSERT_SYS_OK(dfd);

    int fd = openat(dfd, "a.txt", O_CREAT | O_TRUNC | O_RDWR, 0644);
    ASSERT_SYS_OK(fd);

    ssize_t nr = write(fd, "hello", 5);
    ASSERT_TRUE(nr == 5, "write expected 5, got %zd", nr);
    ASSERT_SYS_OK(close(fd));

    struct stat st;
    ASSERT_SYS_OK(fstatat(dfd, "a.txt", &st, 0));
    ASSERT_TRUE(st.st_size == 5, "fstatat size expected 5, got %lld",
                (long long)st.st_size);

    ASSERT_SYS_OK(renameat(dfd, "a.txt", dfd, "b.txt"));
    ASSERT_SYS_OK(fstatat(dfd, "b.txt", &st, 0));
    ASSERT_TRUE(st.st_size == 5, "renamed file size expected 5, got %lld",
                (long long)st.st_size);

    ASSERT_SYS_OK(unlinkat(dfd, "b.txt", 0));

    errno = 0;
    ASSERT_TRUE(fstatat(dfd, "b.txt", &st, 0) == -1 && errno == ENOENT,
                "fstatat after unlink should be ENOENT");

    ASSERT_SYS_OK(close(dfd));
    ASSERT_SYS_OK(rmdir(subdir));
    return 0;
}

static int
test_dup_fcntl_ioctl(const char *workdir)
{
    char path[PATH_MAX];
    ASSERT_SYS_OK(join_path(path, sizeof(path), workdir, "dup_fcntl.txt"));

    int fd = open(path, O_CREAT | O_TRUNC | O_RDWR, 0644);
    ASSERT_SYS_OK(fd);

    ASSERT_TRUE(write(fd, "0123456789", 10) == 10, "write expected 10 bytes");
    ASSERT_SYS_OK(lseek(fd, 0, SEEK_SET));

    int fd2 = dup(fd);
    bool dup_supported = true;
    if (fd2 < 0 && (errno == ENOTSUP || errno == EOPNOTSUPP || errno == ENOSYS)) {
        dup_supported = false;
        fprintf(stderr, "[INFO] dup on this backend is unsupported, skip shared-offset check\n");
    } else {
        ASSERT_SYS_OK(fd2);
    }

    char buf[4] = {0};
    if (dup_supported) {
        ASSERT_TRUE(read(fd, buf, 2) == 2, "read(fd) expected 2 bytes");
        ASSERT_TRUE(memcmp(buf, "01", 2) == 0, "first read mismatch");

        memset(buf, 0, sizeof(buf));
        ASSERT_TRUE(read(fd2, buf, 2) == 2, "read(fd2) expected 2 bytes");
        ASSERT_TRUE(memcmp(buf, "23", 2) == 0,
                    "dup offset should be shared, expected \"23\"");
    }

    int fd_flags = fcntl(fd, F_GETFD);
    ASSERT_SYS_OK(fd_flags);
    ASSERT_SYS_OK(fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC));

    int fd_flags_after = fcntl(fd, F_GETFD);
    ASSERT_SYS_OK(fd_flags_after);
    ASSERT_TRUE((fd_flags_after & FD_CLOEXEC) != 0,
                "FD_CLOEXEC should be set");

    int file_flags = fcntl(fd, F_GETFL);
    ASSERT_SYS_OK(file_flags);
    ASSERT_SYS_OK(fcntl(fd, F_SETFL, file_flags | O_APPEND));

    int file_flags_after = fcntl(fd, F_GETFL);
    ASSERT_SYS_OK(file_flags_after);
    ASSERT_TRUE((file_flags_after & O_APPEND) != 0, "O_APPEND should be set");

    int avail = -1;
    ASSERT_SYS_OK(ioctl(dup_supported ? fd2 : fd, FIONREAD, &avail));
    if (dup_supported) {
        ASSERT_TRUE(avail == 6, "FIONREAD expected 6, got %d", avail);
        ASSERT_SYS_OK(close(fd2));
    } else {
        ASSERT_TRUE(avail == 10, "FIONREAD expected 10, got %d", avail);
    }

    ASSERT_SYS_OK(close(fd));
    ASSERT_SYS_OK(unlink(path));
    return 0;
}

static int
test_readv_writev_pwritev(const char *workdir)
{
    char path[PATH_MAX];
    ASSERT_SYS_OK(join_path(path, sizeof(path), workdir, "iov.txt"));

    int fd = open(path, O_CREAT | O_TRUNC | O_RDWR, 0644);
    ASSERT_SYS_OK(fd);

    struct iovec wiov[3];
    wiov[0].iov_base = "ab";
    wiov[0].iov_len = 2;
    wiov[1].iov_base = "cd";
    wiov[1].iov_len = 2;
    wiov[2].iov_base = "ef";
    wiov[2].iov_len = 2;

    ssize_t nr = writev(fd, wiov, 3);
    ASSERT_TRUE(nr == 6, "writev expected 6, got %zd", nr);

    ASSERT_SYS_OK(lseek(fd, 0, SEEK_SET));

    char a[2] = {0}, b[2] = {0}, c[2] = {0};
    struct iovec riov[3];
    riov[0].iov_base = a;
    riov[0].iov_len = 2;
    riov[1].iov_base = b;
    riov[1].iov_len = 2;
    riov[2].iov_base = c;
    riov[2].iov_len = 2;

    nr = readv(fd, riov, 3);
    ASSERT_TRUE(nr == 6, "readv expected 6, got %zd", nr);
    ASSERT_TRUE(memcmp(a, "ab", 2) == 0 &&
                memcmp(b, "cd", 2) == 0 &&
                memcmp(c, "ef", 2) == 0, "readv content mismatch");

    struct iovec pwiov[2];
    pwiov[0].iov_base = "12";
    pwiov[0].iov_len = 2;
    pwiov[1].iov_base = "34";
    pwiov[1].iov_len = 2;
    nr = pwritev(fd, pwiov, 2, 1);
    ASSERT_TRUE(nr == 4, "pwritev expected 4, got %zd", nr);

    char out[8] = {0};
    nr = pread(fd, out, 6, 0);
    ASSERT_TRUE(nr == 6, "pread expected 6, got %zd", nr);
    ASSERT_TRUE(memcmp(out, "a1234f", 6) == 0, "pwritev content mismatch");

    ASSERT_SYS_OK(close(fd));
    ASSERT_SYS_OK(unlink(path));
    return 0;
}

typedef int (*test_fn)(const char *workdir);

struct test_case {
    const char *name;
    test_fn fn;
};

static int
run_test(const struct test_case *tc, const char *workdir)
{
    int rc = tc->fn(workdir);
    if (rc == 0) {
        printf("[PASS] %s\n", tc->name);
        return 0;
    }
    printf("[FAIL] %s\n", tc->name);
    return -1;
}

int
main(void)
{
    const char *base = getenv("ZVFS_TEST_ROOT");
    if (!base || base[0] == '\0')
        base = "/tmp";

    char workdir[PATH_MAX];
    int n = snprintf(workdir, sizeof(workdir), "%s/zvfs-hook-api-XXXXXX", base);
    if (n < 0 || (size_t)n >= sizeof(workdir)) {
        fprintf(stderr, "workdir template too long\n");
        return 2;
    }

    if (!mkdtemp(workdir)) {
        fprintf(stderr, "mkdtemp(%s) failed: %s\n", workdir, strerror(errno));
        return 2;
    }

    printf("workdir=%s\n", workdir);
    printf("hint: set ZVFS_TEST_ROOT=/zvfs when validating LD_PRELOAD hook path.\n");

    struct test_case tests[] = {
        {"basic_rw_seek_stat", test_basic_rw_seek_stat},
        {"openat_rename_unlink", test_openat_rename_unlink},
        {"dup_fcntl_ioctl", test_dup_fcntl_ioctl},
        {"readv_writev_pwritev", test_readv_writev_pwritev},
    };

    int failed = 0;
    for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i) {
        if (run_test(&tests[i], workdir) != 0)
            failed++;
    }

    const char *keep = getenv("ZVFS_TEST_KEEP");
    if (!keep || strcmp(keep, "1") != 0) {
        if (rmdir(workdir) < 0) {
            fprintf(stderr,
                    "warning: failed to remove workdir %s: %s\n",
                    workdir, strerror(errno));
        }
    } else {
        printf("kept workdir=%s\n", workdir);
    }

    if (failed == 0) {
        printf("ALL TESTS PASSED\n");
        return 0;
    }

    printf("FAILED=%d\n", failed);
    return 1;
}
