// Copyright 2020-2023 David Robillard <d@drobilla.net>
// SPDX-License-Identifier: ISC

#undef NDEBUG

#include "zix/allocator.h"
#include "zix/filesystem.h"
#include "zix/path.h"
#include "zix/status.h"
#include "zix/string_view.h"

#ifndef _WIN32
#  include <unistd.h>
#endif

#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L
#  include <sys/socket.h>
#  include <sys/stat.h>
#  include <sys/un.h>
#endif

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void
test_temp_directory_path(void)
{
  char* tmpdir = zix_temp_directory_path(NULL);

  assert(zix_file_type(tmpdir) == ZIX_FILE_TYPE_DIRECTORY);

  free(tmpdir);
}

static void
test_current_path(void)
{
  char* cwd = zix_current_path(NULL);

  assert(zix_file_type(cwd) == ZIX_FILE_TYPE_DIRECTORY);

  free(cwd);
}

static char*
create_temp_dir(const char* const name_pattern)
{
  char* const temp         = zix_temp_directory_path(NULL);
  char* const path_pattern = zix_path_join(NULL, temp, name_pattern);
  char* const result       = zix_create_temporary_directory(NULL, path_pattern);
  free(path_pattern);
  zix_free(NULL, temp);
  return result;
}

static void
test_canonical_path(void)
{
  assert(!zix_canonical_path(NULL, NULL));

  char* const temp_dir = create_temp_dir("zixXXXXXX");
  assert(temp_dir);

  char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  assert(file_path);

  {
    FILE* const f = fopen(file_path, "w");
    assert(f);
    fprintf(f, "test\n");
    fclose(f);
  }

#ifndef _WIN32
  // Test symlink resolution

  char* const link_path = zix_path_join(NULL, temp_dir, "zix_test_link");

  assert(!zix_create_symlink(file_path, link_path));

  char* const real_file_path = zix_canonical_path(NULL, file_path);
  char* const real_link_path = zix_canonical_path(NULL, link_path);

  assert(real_file_path);
  assert(real_link_path);
  assert(!strcmp(real_file_path, real_link_path));

  assert(!zix_remove(link_path));
  free(real_link_path);
  free(real_file_path);
  free(link_path);
#endif

  // Test dot segment resolution

  char* const parent_dir_1 = zix_path_join(NULL, temp_dir, "..");
  assert(parent_dir_1);

  const ZixStringView parent_view  = zix_path_parent_path(temp_dir);
  char* const         parent_dir_2 = zix_string_view_copy(NULL, parent_view);
  assert(parent_dir_2);
  assert(parent_dir_2[0]);

  char* const real_parent_dir_1 = zix_canonical_path(NULL, parent_dir_1);
  char* const real_parent_dir_2 = zix_canonical_path(NULL, parent_dir_2);

  assert(real_parent_dir_1);
  assert(real_parent_dir_2);
  assert(!strcmp(real_parent_dir_1, real_parent_dir_2));

  // Test missing files

  assert(!zix_canonical_path(NULL, "/presumuably/absent"));
  assert(!zix_canonical_path(NULL, "/presumuably/absent/"));
  assert(!zix_canonical_path(NULL, "presumuably_absent"));

  // Clean everything up

  assert(!zix_remove(file_path));
  assert(!zix_remove(temp_dir));

  free(real_parent_dir_2);
  free(real_parent_dir_1);
  free(parent_dir_2);
  free(parent_dir_1);
  free(file_path);
  free(temp_dir);
}

static void
test_file_type(void)
{
  char* const temp_dir = create_temp_dir("zixXXXXXX");
  assert(temp_dir);

  char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  assert(file_path);
  assert(zix_file_type(file_path) == ZIX_FILE_TYPE_NONE);

  // Regular file
  FILE* f = fopen(file_path, "w");
  assert(f);
  fprintf(f, "test\n");
  fclose(f);
  assert(zix_file_type(file_path) == ZIX_FILE_TYPE_REGULAR);
  assert(!zix_remove(file_path));

#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L

  // FIFO
  if (!mkfifo(file_path, 0600)) {
    assert(zix_file_type(file_path) == ZIX_FILE_TYPE_FIFO);
    assert(!zix_remove(file_path));
  }

  // Socket
  const int sock = socket(AF_UNIX, SOCK_STREAM, 0);
  if (sock >= 0) {
    const socklen_t           addr_len = sizeof(struct sockaddr_un);
    struct sockaddr_un* const addr = (struct sockaddr_un*)calloc(1, addr_len);

    addr->sun_family = AF_UNIX;
    strncpy(addr->sun_path, file_path, sizeof(addr->sun_path) - 1);

    const int fd = bind(sock, (struct sockaddr*)addr, addr_len);
    if (fd >= 0) {
      assert(zix_file_type(file_path) == ZIX_FILE_TYPE_SOCKET);
      assert(!zix_remove(file_path));
      close(fd);
    }

    close(sock);
    free(addr);
  }

#endif

  assert(!zix_remove(temp_dir));
  free(file_path);
  free(temp_dir);
}

static void
test_path_exists(void)
{
  char* const temp_dir  = create_temp_dir("zixXXXXXX");
  char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  assert(temp_dir);
  assert(file_path);

  assert(zix_file_type(file_path) == ZIX_FILE_TYPE_NONE);

  FILE* f = fopen(file_path, "w");
  assert(f);
  fprintf(f, "test\n");
  fclose(f);

  assert(zix_file_type(file_path) == ZIX_FILE_TYPE_REGULAR);

  assert(!zix_remove(file_path));
  assert(!zix_remove(temp_dir));

  free(file_path);
  free(temp_dir);
}

static void
test_is_directory(void)
{
  char* const temp_dir  = create_temp_dir("zixXXXXXX");
  char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  assert(temp_dir);
  assert(file_path);

  assert(zix_file_type(temp_dir) == ZIX_FILE_TYPE_DIRECTORY);
  assert(zix_file_type(file_path) == ZIX_FILE_TYPE_NONE);

  FILE* f = fopen(file_path, "w");
  assert(f);
  fprintf(f, "test\n");
  fclose(f);

  assert(zix_file_type(file_path) == ZIX_FILE_TYPE_REGULAR);

  assert(!zix_remove(file_path));
  assert(!zix_remove(temp_dir));

  free(file_path);
  free(temp_dir);
}

static int
write_to_path(const char* const path, const char* const contents)
{
  int         ret = -1;
  FILE* const f   = fopen(path, "w");
  if (f) {
    const size_t len = strlen(contents);
    fwrite(contents, 1, len, f);

    ret = fflush(f) ? errno : ferror(f) ? EBADF : fclose(f) ? errno : 0;
  }

  return ret;
}

static void
test_copy_file(const char* data_file_path)
{
  char* const temp_dir      = create_temp_dir("zixXXXXXX");
  char* const tmp_file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  char* const copy_path     = zix_path_join(NULL, temp_dir, "zix_test_copy");
  assert(temp_dir);
  assert(tmp_file_path);
  assert(copy_path);

  if (!data_file_path) {
    data_file_path = tmp_file_path;
  }

  assert(!write_to_path(tmp_file_path, "test\n"));

  assert(zix_copy_file(NULL, tmp_file_path, "/does/not/exist", 0U));
  assert(zix_copy_file(NULL, "/does/not/exist", copy_path, 0U));
  assert(zix_file_type(copy_path) == ZIX_FILE_TYPE_NONE);

  // Fail to copy from/to a directory
  assert(zix_copy_file(NULL, temp_dir, copy_path, 0U));
  assert(zix_copy_file(NULL, tmp_file_path, temp_dir, 0U));

  if (data_file_path) {
    // Fail to copy a file to itself
    assert(zix_copy_file(NULL, data_file_path, data_file_path, 0U) ==
           ZIX_STATUS_EXISTS);

    // Successful copy between filesystems
    assert(!zix_copy_file(NULL, data_file_path, copy_path, 0U));
    assert(zix_file_equals(NULL, data_file_path, copy_path));

    // Trying the same again fails because the copy path already exists
    assert(zix_copy_file(NULL, data_file_path, copy_path, 0U) ==
           ZIX_STATUS_EXISTS);

    // Unless overwriting is requested
    assert(!zix_copy_file(
      NULL, data_file_path, copy_path, ZIX_COPY_OPTION_OVERWRITE_EXISTING));

    assert(!zix_remove(copy_path));
  }

  // Successful copy within a filesystem
  assert(zix_file_type(copy_path) == ZIX_FILE_TYPE_NONE);
  assert(!zix_copy_file(NULL, tmp_file_path, copy_path, 0U));
  assert(zix_file_equals(NULL, tmp_file_path, copy_path));
  assert(!zix_remove(copy_path));

  if (zix_file_type("/dev/random") == ZIX_FILE_TYPE_CHARACTER) {
    // Fail to copy infinite file to a file
    assert(zix_copy_file(NULL, "/dev/random", copy_path, 0U) ==
           ZIX_STATUS_BAD_ARG);

    // Fail to copy infinite file to itself
    assert(zix_copy_file(NULL, "/dev/random", "/dev/random", 0U) ==
           ZIX_STATUS_BAD_ARG);

    // Fail to copy infinite file to another
    assert(zix_copy_file(NULL, "/dev/random", "/dev/urandom", 0U) ==
           ZIX_STATUS_BAD_ARG);
  }

  if (zix_file_type("/dev/full") == ZIX_FILE_TYPE_CHARACTER) {
    if (data_file_path) {
      assert(zix_copy_file(NULL,
                           data_file_path,
                           "/dev/full",
                           ZIX_COPY_OPTION_OVERWRITE_EXISTING) ==
             ZIX_STATUS_NO_SPACE);
    }

    // Copy short file (error after flushing)
    assert(zix_copy_file(
      NULL, tmp_file_path, "/dev/full", ZIX_COPY_OPTION_OVERWRITE_EXISTING));

    // Copy long file (error during writing)
    FILE* const f = fopen(tmp_file_path, "w");
    assert(f);
    for (size_t i = 0; i < 4096; ++i) {
      fprintf(f, "test\n");
    }
    fclose(f);
    assert(zix_copy_file(
      NULL, tmp_file_path, "/dev/full", ZIX_COPY_OPTION_OVERWRITE_EXISTING));
  }

  assert(!zix_remove(tmp_file_path));
  assert(!zix_remove(temp_dir));

  free(copy_path);
  free(tmp_file_path);
  free(temp_dir);
}

static void
test_flock(void)
{
  char* const temp_dir  = create_temp_dir("zixXXXXXX");
  char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  assert(temp_dir);
  assert(file_path);

  FILE* const f1 = fopen(file_path, "w");
  FILE* const f2 = fopen(file_path, "a+");

  assert(f1);
  assert(f2);

  ZixStatus st = zix_file_lock(f1, ZIX_FILE_LOCK_TRY);
  assert(!st || st == ZIX_STATUS_NOT_SUPPORTED);

  if (!st) {
    assert(zix_file_lock(f2, ZIX_FILE_LOCK_TRY) == ZIX_STATUS_UNAVAILABLE);
    assert(!zix_file_unlock(f1, ZIX_FILE_LOCK_TRY));
  }

  fclose(f2);
  fclose(f1);
  assert(!zix_remove(file_path));
  assert(!zix_remove(temp_dir));
  free(file_path);
  free(temp_dir);
}

typedef struct {
  size_t n_names;
  char** names;
} FileList;

static void
visit(const char* const path, const char* const name, void* const data)
{
  (void)path;

  const size_t    name_len  = strlen(name);
  FileList* const file_list = (FileList*)data;

  char** const new_names =
    (char**)realloc(file_list->names, sizeof(char*) * ++file_list->n_names);

  if (new_names) {
    char* const name_copy = (char*)calloc(name_len + 1, 1);
    memcpy(name_copy, name, name_len + 1);

    file_list->names                         = new_names;
    file_list->names[file_list->n_names - 1] = name_copy;
  }
}

static void
test_dir_for_each(void)
{
  char* const temp_dir = create_temp_dir("zixXXXXXX");
  char* const path1    = zix_path_join(NULL, temp_dir, "zix_test_1");
  char* const path2    = zix_path_join(NULL, temp_dir, "zix_test_2");
  assert(temp_dir);
  assert(path1);
  assert(path2);

  FILE* const f1 = fopen(path1, "w");
  FILE* const f2 = fopen(path2, "w");
  assert(f1);
  assert(f2);
  fprintf(f1, "test\n");
  fprintf(f2, "test\n");
  fclose(f2);
  fclose(f1);

  FileList file_list = {0, NULL};
  zix_dir_for_each(temp_dir, &file_list, visit);

  assert(file_list.names);
  assert((!strcmp(file_list.names[0], "zix_test_1") &&
          !strcmp(file_list.names[1], "zix_test_2")) ||
         (!strcmp(file_list.names[0], "zix_test_2") &&
          !strcmp(file_list.names[1], "zix_test_1")));

  assert(!zix_remove(path2));
  assert(!zix_remove(path1));
  assert(!zix_remove(temp_dir));

  free(file_list.names[0]);
  free(file_list.names[1]);
  free(file_list.names);
  free(path2);
  free(path1);
  free(temp_dir);
}

static void
test_create_temporary_directory(void)
{
  assert(!zix_create_temporary_directory(NULL, ""));

  char* const path1 = create_temp_dir("zixXXXXXX");

  assert(path1);
  assert(zix_file_type(path1) == ZIX_FILE_TYPE_DIRECTORY);

  char* const path2 = create_temp_dir("zixXXXXXX");

  assert(path2);
  assert(strcmp(path1, path2));
  assert(zix_file_type(path1) == ZIX_FILE_TYPE_DIRECTORY);
  assert(zix_file_type(path2) == ZIX_FILE_TYPE_DIRECTORY);

  assert(!zix_remove(path2));
  assert(!zix_remove(path1));
  free(path2);
  free(path1);
}

static void
test_create_directory_like(void)
{
  char* const temp_dir = create_temp_dir("zixXXXXXX");
  assert(temp_dir);
  assert(zix_file_type(temp_dir) == ZIX_FILE_TYPE_DIRECTORY);

  char* const sub_dir = zix_path_join(NULL, temp_dir, "sub");
  assert(zix_create_directory_like(sub_dir, sub_dir) == ZIX_STATUS_NOT_FOUND);
  assert(!zix_create_directory_like(sub_dir, temp_dir));
  assert(zix_file_type(sub_dir) == ZIX_FILE_TYPE_DIRECTORY);
  assert(!zix_remove(sub_dir));
  zix_free(NULL, sub_dir);

  assert(!zix_remove(temp_dir));
  free(temp_dir);
}

static void
test_create_directories(void)
{
  char* const temp_dir = create_temp_dir("zixXXXXXX");

  assert(temp_dir);
  assert(zix_file_type(temp_dir) == ZIX_FILE_TYPE_DIRECTORY);
  assert(zix_create_directories(NULL, "") == ZIX_STATUS_BAD_ARG);

  char* const child_dir      = zix_path_join(NULL, temp_dir, "child");
  char* const grandchild_dir = zix_path_join(NULL, child_dir, "grandchild");

  assert(!zix_create_directories(NULL, grandchild_dir));
  assert(zix_file_type(grandchild_dir) == ZIX_FILE_TYPE_DIRECTORY);
  assert(zix_file_type(child_dir) == ZIX_FILE_TYPE_DIRECTORY);

  char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  FILE* const f         = fopen(file_path, "w");

  assert(f);
  fprintf(f, "test\n");
  fclose(f);

  assert(zix_create_directories(NULL, file_path) == ZIX_STATUS_EXISTS);

  assert(!zix_remove(file_path));
  assert(!zix_remove(grandchild_dir));
  assert(!zix_remove(child_dir));
  assert(!zix_remove(temp_dir));
  free(file_path);
  free(child_dir);
  free(grandchild_dir);
  free(temp_dir);
}

static void
test_file_equals(void)
{
  char* const temp_dir = create_temp_dir("zixXXXXXX");
  char* const path1    = zix_path_join(NULL, temp_dir, "zix1");
  char* const path2    = zix_path_join(NULL, temp_dir, "zix2");
  assert(temp_dir);

  // Equal: test, test
  assert(!write_to_path(path1, "test"));
  assert(!write_to_path(path2, "test"));
  assert(zix_file_equals(NULL, path1, path2));

  // Missing files
  assert(!zix_file_equals(NULL, path1, "/does/not/exist"));
  assert(!zix_file_equals(NULL, "/does/not/exist", path2));

  // Longer RHS: test, testdiff
  assert(!write_to_path(path2, "diff"));
  assert(!zix_file_equals(NULL, path1, path2));

  // Longer LHS: testdifflong, testdiff
  assert(!write_to_path(path1, "difflong"));
  assert(!zix_file_equals(NULL, path1, path2));

  // Equal sizes but different content: testdifflong, testdifflang
  assert(!write_to_path(path2, "difflang"));
  assert(!zix_file_equals(NULL, path1, path2));

  assert(zix_file_equals(NULL, path1, path1));
  assert(zix_file_equals(NULL, path2, path2));

  assert(!zix_remove(path2));
  assert(!zix_remove(path1));
  assert(!zix_remove(temp_dir));

  free(path2);
  free(path1);
  free(temp_dir);
}

static void
test_file_size(void)
{
  static const char* const contents = "file size test";

  char* const temp_dir = create_temp_dir("zixXXXXXX");
  char* const path     = zix_path_join(NULL, temp_dir, "zix_test");

  assert(temp_dir);
  assert(!write_to_path(path, contents));

  const ZixFileOffset size = zix_file_size(path);
  assert(size > 0);
  assert((size_t)size == strlen(contents));

  assert(!zix_remove(path));
  assert(!zix_remove(temp_dir));

  free(path);
  free(temp_dir);
}

static void
test_create_symlink(void)
{
  static const char* const contents = "zixtest";

  char* const temp_dir = create_temp_dir("zixXXXXXX");
  assert(temp_dir);

  // Write contents to original file
  char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  {
    FILE* const f = fopen(file_path, "w");
    assert(f);
    fprintf(f, "%s", contents);
    fclose(f);
  }

  // Ensure original file exists and is a regular file
  assert(zix_symlink_type(file_path) == ZIX_FILE_TYPE_REGULAR);

  // Create symlink to original file
  char* const     link_path = zix_path_join(NULL, temp_dir, "zix_test_link");
  const ZixStatus st        = zix_create_symlink(file_path, link_path);

  // Check that the symlink seems equivalent to the original file
  assert(!st || st == ZIX_STATUS_NOT_SUPPORTED || st == ZIX_STATUS_BAD_PERMS);
  assert(!st || zix_symlink_type(link_path) == ZIX_FILE_TYPE_NONE);
  if (!st) {
    assert(zix_symlink_type(link_path) == ZIX_FILE_TYPE_SYMLINK);
    assert(zix_file_type(link_path) == ZIX_FILE_TYPE_REGULAR);
    assert(zix_file_equals(NULL, file_path, link_path));
    assert(!zix_remove(link_path));
  }

  assert(!zix_remove(file_path));
  assert(!zix_remove(temp_dir));

  free(link_path);
  free(file_path);
  free(temp_dir);
}

static void
test_create_directory_symlink(void)
{
  char* const temp_dir = create_temp_dir("zixXXXXXX");
  assert(temp_dir);

  char* const     link_path = zix_path_join(NULL, temp_dir, "zix_test_link");
  const ZixStatus st        = zix_create_directory_symlink(temp_dir, link_path);

  if (st != ZIX_STATUS_NOT_SUPPORTED && st != ZIX_STATUS_BAD_PERMS) {
    assert(!st);
    assert(zix_file_type(link_path) == ZIX_FILE_TYPE_DIRECTORY);

#ifdef _WIN32
    assert(zix_symlink_type(link_path) == ZIX_FILE_TYPE_DIRECTORY);
#else
    assert(zix_symlink_type(link_path) == ZIX_FILE_TYPE_SYMLINK);
#endif

    assert(!zix_remove(link_path));
  }

  assert(!zix_remove(temp_dir));
  free(link_path);
  free(temp_dir);
}

static void
test_create_hard_link(void)
{
  static const char* const contents = "zixtest";

  char* const temp_dir = create_temp_dir("zixXXXXXX");
  assert(temp_dir);

  // Write contents to original file
  char* const file_path = zix_path_join(NULL, temp_dir, "zix_test_file");
  {
    FILE* const f = fopen(file_path, "w");
    assert(f);
    fprintf(f, "%s", contents);
    fclose(f);
  }

  // Ensure original file exists and is a regular file
  assert(zix_symlink_type(file_path) == ZIX_FILE_TYPE_REGULAR);

  // Create symlink to original file
  char* const     link_path = zix_path_join(NULL, temp_dir, "zix_test_link");
  const ZixStatus st        = zix_create_hard_link(file_path, link_path);

  // Check that the link is equivalent to the original file
  assert(!st || st == ZIX_STATUS_NOT_SUPPORTED || st == ZIX_STATUS_MAX_LINKS);
  assert(!st || zix_symlink_type(link_path) == ZIX_FILE_TYPE_NONE);
  if (!st) {
    assert(zix_file_type(link_path) == ZIX_FILE_TYPE_REGULAR);
    assert(zix_file_equals(NULL, file_path, link_path));
    assert(!zix_remove(link_path));
  }

  assert(!zix_remove(file_path));
  assert(!zix_remove(temp_dir));

  free(link_path);
  free(file_path);
  free(temp_dir);
}

int
main(const int argc, char** const argv)
{
  // Try to find some existing data file that's ideally not on a tmpfs
  const char* data_file_path = (argc > 1) ? argv[1] : "build.ninja";
  if (zix_file_type(data_file_path) != ZIX_FILE_TYPE_REGULAR) {
    data_file_path = NULL;
  }

  test_temp_directory_path();
  test_current_path();
  test_canonical_path();
  test_file_type();
  test_path_exists();
  test_is_directory();
  test_copy_file(data_file_path);
  test_flock();
  test_dir_for_each();
  test_create_temporary_directory();
  test_create_directory_like();
  test_create_directories();
  test_file_equals();
  test_file_size();
  test_create_symlink();
  test_create_directory_symlink();
  test_create_hard_link();

  return 0;
}