/*
 * We modified musl-fts not to use fchdir() and we made FTS_NOCHDIR
 * the default behavior. This test is to make sure that the modified
 * musl-fts works as expected.
 */

#include <errno.h>
#include <fts.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

void __expect_next_ftsent(FTSENT *p, const char *path, short level, int info,
                          const char *file, int line) {
  if (p == NULL) {
    printf("Error: fts_read() returned NULL at %s:%d\n", file, line);
    exit(1);
  }

  if (strcmp(p->fts_path, path) != 0) {
    printf("Error: expected path \"%s\", got \"%s\" at %s:%d\n", path,
           p->fts_path, file, line);
    exit(1);
  }

  if (p->fts_level != level) {
    printf("Error: expected level %d, got %d at %s:%d\n", level, p->fts_level,
           file, line);
    exit(1);
  }

  if (p->fts_info != info) {
    printf("Error: expected info %d, got %d at %s:%d\n", info, p->fts_info,
           file, line);
    exit(1);
  }
}

#define expect_next_ftsent(p, path, level, info)                               \
  __expect_next_ftsent(p, path, level, info, __FILE__, __LINE__)

static int compare_ents(const FTSENT **a, const FTSENT **b) {
  return strcmp((*a)->fts_name, (*b)->fts_name);
}

void touch(const char *path) {
  FILE *f = fopen(path, "w");
  if (f == NULL) {
    printf("Error: fopen(%s) failed: %s\n", path, strerror(errno));
    exit(1);
  }
  fclose(f);
}

void test_fts_info(int base_options) {
  FTS *ftsp;

  mkdir("t1", 0755);
  touch("t1/file1");
  mkdir("t1/dir1", 0755);
  touch("t1/dir1/file2");
  mkdir("t1/dir1/dir2", 0755);
  touch("t1/dir1/file3");

  char *paths[] = {"./t1", NULL};
  ftsp = fts_open(paths, base_options, compare_ents);
  if (ftsp == NULL) {
    printf("Error: fts_open(%s, %d) failed: %s\n", paths[0], base_options,
           strerror(errno));
    exit(1);
  }

  expect_next_ftsent(fts_read(ftsp), "./t1", 0, FTS_D);
  expect_next_ftsent(fts_read(ftsp), "./t1/dir1", 1, FTS_D);
  expect_next_ftsent(fts_read(ftsp), "./t1/dir1/dir2", 2, FTS_D);
  expect_next_ftsent(fts_read(ftsp), "./t1/dir1/dir2", 2, FTS_DP);
  expect_next_ftsent(fts_read(ftsp), "./t1/dir1/file2", 2, FTS_F);
  expect_next_ftsent(fts_read(ftsp), "./t1/dir1/file3", 2, FTS_F);
  expect_next_ftsent(fts_read(ftsp), "./t1/dir1", 1, FTS_DP);
  expect_next_ftsent(fts_read(ftsp), "./t1/file1", 1, FTS_F);
  expect_next_ftsent(fts_read(ftsp), "./t1", 0, FTS_DP);

  fts_close(ftsp);
}

void test_symlink_fts_info(int base_options) {
  FTS *ftsp;

  mkdir("t2", 0755);
  touch("t2/file1");
  symlink("file1", "t2/symlink1");
  symlink("nonexistent", "t2/broken_symlink1");

  char *paths[] = {"./t2", NULL};
  ftsp = fts_open(paths, base_options, compare_ents);
  if (ftsp == NULL) {
    printf("Error: fts_open(%s, %d) failed: %s\n", paths[0], base_options,
           strerror(errno));
    exit(1);
  }

  expect_next_ftsent(fts_read(ftsp), "./t2", 0, FTS_D);
  expect_next_ftsent(fts_read(ftsp), "./t2/broken_symlink1", 1, FTS_SL);
  expect_next_ftsent(fts_read(ftsp), "./t2/file1", 1, FTS_F);
  expect_next_ftsent(fts_read(ftsp), "./t2/symlink1", 1, FTS_SL);
  expect_next_ftsent(fts_read(ftsp), "./t2", 0, FTS_DP);

  fts_close(ftsp);

  ftsp = fts_open(paths, base_options | FTS_LOGICAL, compare_ents);
  if (ftsp == NULL) {
    printf("Error: fts_open(%s, %d | FTS_LOGICAL) failed: %s\n", paths[0],
           base_options, strerror(errno));
    exit(1);
  }

  expect_next_ftsent(fts_read(ftsp), "./t2", 0, FTS_D);
  // FTS_SLNONE should be returned for broken symlinks in FTS_LOGICAL mode
  expect_next_ftsent(fts_read(ftsp), "./t2/broken_symlink1", 1, FTS_SLNONE);
  expect_next_ftsent(fts_read(ftsp), "./t2/file1", 1, FTS_F);
  expect_next_ftsent(fts_read(ftsp), "./t2/symlink1", 1, FTS_F);
  expect_next_ftsent(fts_read(ftsp), "./t2", 0, FTS_DP);
}

void __expect_child_ftsent(FTSENT *p, const char *name, short level, int info,
                           const char *file, int line) {
  if (p == NULL) {
    printf("Error: fts_children() returned NULL at %s:%d\n", file, line);
    exit(1);
  }

  // Check fts_name instead of fts_path because fts_children() doesn't set
  // fts_path
  if (strcmp(p->fts_name, name) != 0) {
    printf("Error: expected name \"%s\", got \"%s\" at %s:%d\n", name,
           p->fts_name, file, line);
    exit(1);
  }

  if (p->fts_level != level) {
    printf("Error: expected level %d, got %d at %s:%d\n", level, p->fts_level,
           file, line);
    exit(1);
  }

  if (p->fts_info != info) {
    printf("Error: expected info %d, got %d at %s:%d\n", info, p->fts_info,
           file, line);
    exit(1);
  }
}

#define expect_child_ftsent(p, name, level, info)                              \
  __expect_child_ftsent(p, name, level, info, __FILE__, __LINE__)

void test_fts_children(int base_options) {
  FTS *ftsp;
  FTSENT *p;

  mkdir("t3", 0755);
  touch("t3/file1");
  mkdir("t3/dir1", 0755);
  touch("t3/dir1/file2");
  mkdir("t3/dir1/dir2", 0755);
  touch("t3/dir1/file3");

  char *paths[] = {"./t3", NULL};
  ftsp = fts_open(paths, base_options, compare_ents);
  if (ftsp == NULL) {
    printf("Error: fts_open(%s, %d) failed: %s\n", paths[0], base_options,
           strerror(errno));
    exit(1);
  }

  FTSENT *t3 = fts_read(ftsp);
  expect_next_ftsent(t3, "./t3", 0, FTS_D);

  FTSENT *ents = fts_children(ftsp, 0);
  expect_child_ftsent(ents, "dir1", 1, FTS_D);
  expect_child_ftsent(ents->fts_link, "file1", 1, FTS_F);

  fts_close(ftsp);
}

int main(void) {
  int base_options_set[] = {FTS_PHYSICAL, FTS_NOCHDIR};
  for (int i = 0; i < sizeof(base_options_set) / sizeof(base_options_set[0]);
       i++) {
    test_fts_info(base_options_set[i]);
    test_symlink_fts_info(base_options_set[i]);
    test_fts_children(base_options_set[i]);
  }
  return 0;
}
