// SPDX-License-Identifier: GPL-3.0-or-later

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

#include "parse.h"
#include "specifier.h"

#define TEST(name) static void name(void)

#define RUN(name)				\
  do {						\
    printf("  %-40s", #name "...");		\
    name();					\
    printf(" OK\n");				\
  } while (0)

// helper: write a string to a temp file and return the path
static char *write_temp_config(const char *content) {
  static char tmpl[256];
  snprintf(tmpl, sizeof(tmpl), "/tmp/seedfiles-test-XXXXXX.conf");
  int fd = mkstemps(tmpl, 5);
  assert(fd >= 0);
  size_t len = strlen(content);
  assert(write(fd, content, len) == (ssize_t)len);
  close(fd);
  return tmpl;
}

// item_type_is_glob

TEST(test_item_type_is_glob) {
  // non-glob types
  assert(!item_type_is_glob(CREATE_FILE));
  assert(!item_type_is_glob(TRUNCATE_FILE));
  assert(!item_type_is_glob(CREATE_DIRECTORY));
  assert(!item_type_is_glob(TRUNCATE_DIRECTORY));
  assert(!item_type_is_glob(CREATE_FIFO));
  assert(!item_type_is_glob(CREATE_SYMLINK));
  assert(!item_type_is_glob(CREATE_CHAR_DEVICE));
  assert(!item_type_is_glob(CREATE_BLOCK_DEVICE));
  assert(!item_type_is_glob(COPY_FILES));

  // glob types
  assert(item_type_is_glob(SET_XATTR));
  assert(item_type_is_glob(RECURSIVE_SET_XATTR));
  assert(item_type_is_glob(SET_ACL));
  assert(item_type_is_glob(RECURSIVE_SET_ACL));
  assert(item_type_is_glob(SET_ATTRIBUTE));
  assert(item_type_is_glob(RECURSIVE_SET_ATTRIBUTE));
  assert(item_type_is_glob(IGNORE_PATH));
  assert(item_type_is_glob(IGNORE_DIRECTORY_PATH));
  assert(item_type_is_glob(REMOVE_PATH));
  assert(item_type_is_glob(RECURSIVE_REMOVE_PATH));
  assert(item_type_is_glob(RELABEL_PATH));
  assert(item_type_is_glob(RECURSIVE_RELABEL_PATH));
  assert(item_type_is_glob(ADJUST_MODE));
}

TEST(test_context_lifecycle) {
  Context c;
  context_init(&c);
  assert(c.items != NULL);
  assert(c.globs != NULL);
  context_done(&c);
}

TEST(test_parse_directory) {
  char *path = write_temp_config("d /tmp/test-dir 0755 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/test-dir");
  assert(a != NULL);
  assert(a->n_items == 1);
  assert(a->items[0].type == CREATE_DIRECTORY);
  assert(streq(a->items[0].path, "/tmp/test-dir"));
  assert(a->items[0].mode_set);
  assert(a->items[0].mode == 0755);
  assert(!a->items[0].uid_set);
  assert(!a->items[0].gid_set);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_file_with_content) {
  char *path = write_temp_config("f /tmp/test-file 0644 - - - hello world\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/test-file");
  assert(a != NULL);
  assert(a->n_items == 1);
  assert(a->items[0].type == CREATE_FILE);
  assert(streq(a->items[0].argument, "hello world"));
  assert(a->items[0].mode == 0644);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_symlink) {
  char *path = write_temp_config("L /tmp/test-link - - - - /tmp/target\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/test-link");
  assert(a != NULL);
  assert(a->items[0].type == CREATE_SYMLINK);
  assert(streq(a->items[0].argument, "/tmp/target"));

  context_done(&c);
  unlink(path);
}

TEST(test_parse_remove_path) {
  char *path = write_temp_config("r /tmp/removeme\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  // r is a glob type
  ItemArray *a = ordered_hashmap_get(c.globs, "/tmp/removeme");
  assert(a != NULL);
  assert(a->items[0].type == REMOVE_PATH);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_recursive_remove) {
  char *path = write_temp_config("R /tmp/removedir\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.globs, "/tmp/removedir");
  assert(a != NULL);
  assert(a->items[0].type == RECURSIVE_REMOVE_PATH);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_force_modifier) {
  char *path = write_temp_config("d+ /tmp/force-dir 0755 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/force-dir");
  assert(a != NULL);
  assert(a->items[0].append_or_force);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_allow_failure) {
  char *path = write_temp_config("d- /tmp/allow-fail 0755 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/allow-fail");
  assert(a != NULL);
  assert(a->items[0].allow_failure);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_combined_modifiers) {
  char *path = write_temp_config("d+- /tmp/combo 0755 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/combo");
  assert(a != NULL);
  assert(a->items[0].append_or_force);
  assert(a->items[0].allow_failure);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_age) {
  char *path = write_temp_config("d /tmp/aged 0755 - - 10d\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/aged");
  assert(a != NULL);
  assert(a->items[0].age_set);
  assert(a->items[0].age == 10 * USEC_PER_DAY);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_user_group) {
  char *path = write_temp_config("d /tmp/owned 0755 root root -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/owned");
  assert(a != NULL);
  assert(a->items[0].uid_set);
  assert(a->items[0].uid == 0);
  assert(a->items[0].gid_set);
  assert(a->items[0].gid == 0);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_comments_blanks) {
  char *path = write_temp_config("# this is a comment\n"
                                 "\n"
                                 "  # indented comment\n"
                                 "d /tmp/real-entry 0755 - - -\n"
                                 "\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  // only one entry should exist
  assert(ordered_hashmap_get(c.items, "/tmp/real-entry") != NULL);
  assert(ordered_hashmap_size(c.items) == 1);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_multiple_entries) {
  char *path = write_temp_config("d /tmp/dir1 0755 - - -\n"
                                 "d /tmp/dir2 0700 - - -\n"
                                 "f /tmp/file1 0644 - - - content\n"
                                 "L /tmp/link1 - - - - /target\n"
                                 "r /tmp/removeme\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  // 3 non-glob entries (d, d, f, L)
  assert(ordered_hashmap_size(c.items) == 4);
  // 1 glob entry (r)
  assert(ordered_hashmap_size(c.globs) == 1);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_duplicate_paths) {
  char *path = write_temp_config("d /tmp/dup-test 0755 - - -\n"
                                 "z /tmp/dup-test 0700 root root -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  // d goes into items, z goes into globs
  ItemArray *items_a = ordered_hashmap_get(c.items, "/tmp/dup-test");
  ItemArray *globs_a = ordered_hashmap_get(c.globs, "/tmp/dup-test");
  assert(items_a != NULL);
  assert(globs_a != NULL);
  assert(items_a->n_items == 1);
  assert(globs_a->n_items == 1);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_subvolume_types) {
  char *path = write_temp_config("v /tmp/subvol1 0755 - - -\n"
                                 "q /tmp/subvol2 0755 - - -\n"
                                 "Q /tmp/subvol3 0755 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  const char *subvol_paths[] = {"/tmp/subvol1", "/tmp/subvol2", "/tmp/subvol3"};
  for (size_t i = 0; i < 3; i++) {
    ItemArray *a = ordered_hashmap_get(c.items, subvol_paths[i]);
    assert(a != NULL);
    assert(a->n_items == 1);
    // they should be stored with their original type
  }

  context_done(&c);
  unlink(path);
}

TEST(test_parse_mask_perms) {
  char *path = write_temp_config("d /tmp/masked ~0755 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/tmp/masked");
  assert(a != NULL);
  assert(a->items[0].mask_perms);
  assert(a->items[0].mode == 0755);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_mode_only_create) {
  char *path = write_temp_config("d /tmp/create-only 0755:0700 - - -\n");
  Context c;
  context_init(&c);

  // this might not be supported; if so, the mode should still parse
  int r = parse_config_file(&c, path);
  // either succeeds or we silently handle it
  (void)r;

  context_done(&c);
  unlink(path);
}

TEST(test_link_parents) {
  char *path = write_temp_config("d /tmp 0755 - - -\n"
                                 "d /tmp/parent 0755 - - -\n"
                                 "d /tmp/parent/child 0755 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);
  context_link_parents(&c);

  ItemArray *root = ordered_hashmap_get(c.items, "/tmp");
  ItemArray *parent = ordered_hashmap_get(c.items, "/tmp/parent");
  ItemArray *child = ordered_hashmap_get(c.items, "/tmp/parent/child");

  assert(root != NULL);
  assert(parent != NULL);
  assert(child != NULL);

  // root should have no parent
  assert(root->parent == NULL);

  // parent's parent should be root
  assert(parent->parent == root);

  // child's parent should be parent
  assert(child->parent == parent);

  // root should have parent as a child
  bool found = false;
  for (size_t i = 0; i < root->n_children; i++) {
    if (root->children[i] == parent)
      found = true;
  }
  assert(found);

  context_done(&c);
  unlink(path);
}

TEST(test_context_lookup) {
  char *path = write_temp_config("d /tmp/lookup-test 0755 - - -\n"
                                 "z /tmp/lookup-glob 0644 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  assert(context_lookup(&c, "/tmp/lookup-test") != NULL);
  assert(context_lookup(&c, "/tmp/lookup-glob") != NULL);
  assert(context_lookup(&c, "/tmp/nonexistent") == NULL);

  context_done(&c);
  unlink(path);
}

TEST(test_specifier_expand_hostname) {
  _cleanup_free_ char *out = NULL;
  int r = specifier_expand("prefix-%H-suffix", false, &out);
  assert(r == 0);
  assert(out != NULL);
  // should not contain %H anymore
  assert(strstr(out, "%H") == NULL);
  assert(startswith(out, "prefix-") != NULL);
  assert(endswith(out, "-suffix"));
}

TEST(test_specifier_expand_no_specifiers) {
  _cleanup_free_ char *out = NULL;
  int r = specifier_expand("/tmp/plain-path", false, &out);
  assert(r == 0);
  assert(streq(out, "/tmp/plain-path"));
}

TEST(test_parse_invalid_type) {
  // suppress error output for this test
  int saved = log_level;
  log_level = LOG_ERROR + 1;

  char *path = write_temp_config("W /tmp/invalid 0755 - - -\n");
  Context c;
  context_init(&c);

  // should fail or skip the invalid line
  parse_config_file(&c, path);

  // should be empty since W is not a valid type
  assert(ordered_hashmap_size(c.items) == 0);
  assert(ordered_hashmap_size(c.globs) == 0);

  context_done(&c);
  unlink(path);
  log_level = saved;
}

TEST(test_parse_missing_path) {
  int saved = log_level;
  log_level = LOG_ERROR + 1;

  char *path = write_temp_config("d\n");
  Context c;
  context_init(&c);

  parse_config_file(&c, path);
  assert(ordered_hashmap_size(c.items) == 0);

  context_done(&c);
  unlink(path);
  log_level = saved;
}

TEST(test_parse_device_node) {
  char *path = write_temp_config("c /dev/testdev 0660 root root - 1:3\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.items, "/dev/testdev");
  assert(a != NULL);
  assert(a->items[0].type == CREATE_CHAR_DEVICE);
  assert(a->items[0].mode == 0660);

  context_done(&c);
  unlink(path);
}

TEST(test_parse_write_file_basic) {
  char *path = write_temp_config("w /tmp/test-write 0644 - - - some data\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  // w is a glob type, goes into globs hashmap
  ItemArray *a = ordered_hashmap_get(c.globs, "/tmp/test-write");
  assert(a != NULL);
  assert(a->n_items == 1);
  assert(a->items[0].type == WRITE_FILE);
  assert(streq(a->items[0].path, "/tmp/test-write"));
  assert(streq(a->items[0].argument, "some data"));

  context_done(&c);
  unlink(path);
}

TEST(test_parse_write_file_append) {
  char *path = write_temp_config("w+ /tmp/test-append 0644 - - - appended\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  ItemArray *a = ordered_hashmap_get(c.globs, "/tmp/test-append");
  assert(a != NULL);
  assert(a->items[0].type == WRITE_FILE);
  assert(a->items[0].append_or_force);
  assert(streq(a->items[0].argument, "appended"));

  context_done(&c);
  unlink(path);
}

TEST(test_parse_write_file_no_argument) {
  int saved = log_level;
  log_level = LOG_ERROR + 1;

  char *path = write_temp_config("w /tmp/test-noarg 0644 - - -\n");
  Context c;
  context_init(&c);

  parse_config_file(&c, path);

  // should fail parse — argument is required for w
  assert(ordered_hashmap_size(c.items) == 0);
  assert(ordered_hashmap_size(c.globs) == 0);

  context_done(&c);
  unlink(path);
  log_level = saved;
}

TEST(test_parse_write_file_is_glob_type) {
  assert(item_type_is_glob(WRITE_FILE));
}

TEST(test_parse_var_run_patch) {
  char *path = write_temp_config("d /var/run/testdir 0755 - - -\n");
  Context c;
  context_init(&c);

  assert(parse_config_file(&c, path) == 0);

  // should be patched to /run/testdir
  assert(ordered_hashmap_get(c.items, "/run/testdir") != NULL);
  assert(ordered_hashmap_get(c.items, "/var/run/testdir") == NULL);

  context_done(&c);
  unlink(path);
}

int main(void) {
  printf("test-parse:\n");
  RUN(test_item_type_is_glob);
  RUN(test_context_lifecycle);
  RUN(test_parse_directory);
  RUN(test_parse_file_with_content);
  RUN(test_parse_symlink);
  RUN(test_parse_remove_path);
  RUN(test_parse_recursive_remove);
  RUN(test_parse_force_modifier);
  RUN(test_parse_allow_failure);
  RUN(test_parse_combined_modifiers);
  RUN(test_parse_age);
  RUN(test_parse_user_group);
  RUN(test_parse_comments_blanks);
  RUN(test_parse_multiple_entries);
  RUN(test_parse_duplicate_paths);
  RUN(test_parse_subvolume_types);
  RUN(test_parse_mask_perms);
  RUN(test_parse_mode_only_create);
  RUN(test_link_parents);
  RUN(test_context_lookup);
  RUN(test_specifier_expand_hostname);
  RUN(test_specifier_expand_no_specifiers);
  RUN(test_parse_invalid_type);
  RUN(test_parse_missing_path);
  RUN(test_parse_device_node);
  RUN(test_parse_write_file_basic);
  RUN(test_parse_write_file_append);
  RUN(test_parse_write_file_no_argument);
  RUN(test_parse_write_file_is_glob_type);
  RUN(test_parse_var_run_patch);
  printf("All tests passed.\n");
  return EXIT_SUCCESS;
}
