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

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

#include "util.h"

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

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

// string utilities

TEST(test_startswith) {
  assert(startswith("hello world", "hello") != NULL);
  assert(streq(startswith("hello world", "hello"), " world"));
  assert(startswith("hello", "hello") != NULL);
  assert(streq(startswith("hello", "hello"), ""));
  assert(startswith("hello", "world") == NULL);
  assert(startswith("hi", "hello") == NULL);
  assert(startswith("", "") != NULL);
  assert(startswith("anything", "") != NULL);
  assert(startswith("", "x") == NULL);
}

TEST(test_endswith) {
  assert(endswith("hello world", "world"));
  assert(endswith("hello", "hello"));
  assert(!endswith("hello", "world"));
  assert(!endswith("hi", "hello"));
  assert(endswith("", ""));
  assert(endswith("anything", ""));
  assert(!endswith("", "x"));
}

TEST(test_strstrip) {
  char buf1[] = "  hello  ";
  assert(streq(strstrip(buf1), "hello"));

  char buf2[] = "hello";
  assert(streq(strstrip(buf2), "hello"));

  char buf3[] = "   ";
  assert(streq(strstrip(buf3), ""));

  char buf4[] = "";
  assert(streq(strstrip(buf4), ""));

  char buf5[] = "\t\n hello \t\n ";
  assert(streq(strstrip(buf5), "hello"));
}

TEST(test_xstrdup) {
  char *s = xstrdup("hello");
  assert(s != NULL);
  assert(streq(s, "hello"));
  free(s);

  assert(xstrdup(NULL) == NULL);
}

TEST(test_xstrndup) {
  char *s = xstrndup("hello world", 5);
  assert(s != NULL);
  assert(streq(s, "hello"));
  free(s);

  s = xstrndup("hi", 10);
  assert(s != NULL);
  assert(streq(s, "hi"));
  free(s);

  assert(xstrndup(NULL, 5) == NULL);
}

TEST(test_xasprintf) {
  _cleanup_free_ char *s = xasprintf("hello %s %d", "world", 42);
  assert(s != NULL);
  assert(streq(s, "hello world 42"));
}

TEST(test_cunescape) {
  _cleanup_free_ char *r = NULL;

  assert(cunescape("hello", &r) == 0);
  assert(streq(r, "hello"));
  free(r);
  r = NULL;

  assert(cunescape("hello\\nworld", &r) == 0);
  assert(streq(r, "hello\nworld"));
  free(r);
  r = NULL;

  assert(cunescape("tab\\there", &r) == 0);
  assert(streq(r, "tab\there"));
  free(r);
  r = NULL;

  assert(cunescape("\\\\slash", &r) == 0);
  assert(streq(r, "\\slash"));
  free(r);
  r = NULL;

  assert(cunescape("", &r) == 0);
  assert(streq(r, ""));
}

TEST(test_extract_first_word_basic) {
  const char *p = "hello world foo";
  _cleanup_free_ char *w = NULL;

  assert(extract_first_word(&p, &w, NULL, 0) == 1);
  assert(streq(w, "hello"));
  free(w);
  w = NULL;

  assert(extract_first_word(&p, &w, NULL, 0) == 1);
  assert(streq(w, "world"));
  free(w);
  w = NULL;

  assert(extract_first_word(&p, &w, NULL, 0) == 1);
  assert(streq(w, "foo"));
  free(w);
  w = NULL;

  assert(extract_first_word(&p, &w, NULL, 0) == 0);
  assert(w == NULL);
}

TEST(test_extract_first_word_quotes) {
  const char *p = "\"hello world\" foo";
  _cleanup_free_ char *w = NULL;

  assert(extract_first_word(&p, &w, NULL, EXTRACT_UNQUOTE) == 1);
  assert(streq(w, "hello world"));
  free(w);
  w = NULL;

  assert(extract_first_word(&p, &w, NULL, EXTRACT_UNQUOTE) == 1);
  assert(streq(w, "foo"));
}

TEST(test_extract_first_word_cunescape) {
  const char *p = "hello\\nworld";
  _cleanup_free_ char *w = NULL;

  assert(extract_first_word(&p, &w, NULL, EXTRACT_CUNESCAPE) == 1);
  assert(streq(w, "hello\nworld"));
}

TEST(test_extract_first_word_empty) {
  const char *p = "   ";
  _cleanup_free_ char *w = NULL;

  assert(extract_first_word(&p, &w, NULL, 0) == 0);
  assert(w == NULL);
}

TEST(test_extract_first_word_custom_sep) {
  const char *p = "key=value";
  _cleanup_free_ char *w = NULL;

  assert(extract_first_word(&p, &w, "=", 0) == 1);
  assert(streq(w, "key"));
  free(w);
  w = NULL;

  assert(extract_first_word(&p, &w, "=", 0) == 1);
  assert(streq(w, "value"));
}

TEST(test_path_is_absolute) {
  assert(path_is_absolute("/foo"));
  assert(path_is_absolute("/"));
  assert(!path_is_absolute("foo"));
  assert(!path_is_absolute(""));
  assert(!path_is_absolute(NULL));
}

TEST(test_path_simplify) {
  char buf1[] = "/a//b///c";
  assert(streq(path_simplify(buf1), "/a/b/c"));

  char buf2[] = "/a/./b/../c";
  // path_simplify removes double slashes and dots, but not ..
  assert(streq(path_simplify(buf2), "/a/./b/../c") ||
         streq(path_simplify(buf2), "/a/b/../c"));

  char buf3[] = "/";
  assert(streq(path_simplify(buf3), "/"));

  char buf4[] = "///";
  assert(streq(path_simplify(buf4), "/"));
}

TEST(test_path_join) {
  _cleanup_free_ char *p1 = path_join("/foo", "bar");
  assert(streq(p1, "/foo/bar"));

  _cleanup_free_ char *p2 = path_join("/foo/", "bar");
  assert(streq(p2, "/foo/bar"));

  _cleanup_free_ char *p3 = path_join("/", "bar");
  assert(streq(p3, "/bar"));

  _cleanup_free_ char *p4 = path_join("/foo", "/bar");
  // path_join should handle this case
  assert(p4 != NULL);
}

TEST(test_path_startswith) {
  assert(path_startswith("/foo/bar", "/foo") != NULL);
  assert(path_startswith("/foo/bar", "/foo/bar") != NULL);
  assert(path_startswith("/foo/bar", "/") != NULL);
  assert(path_startswith("/foo/bar", "/foo/baz") == NULL);
  assert(path_startswith("/foo", "/foo/bar") == NULL);
}

TEST(test_path_parent) {
  _cleanup_free_ char *p1 = path_parent("/foo/bar");
  assert(streq(p1, "/foo"));

  _cleanup_free_ char *p2 = path_parent("/foo");
  assert(streq(p2, "/"));

  _cleanup_free_ char *p3 = path_parent("/");
  assert(p3 == NULL);
}

TEST(test_path_basename) {
  assert(streq(path_basename("/foo/bar"), "bar"));
  assert(streq(path_basename("/foo"), "foo"));
  assert(streq(path_basename("bar"), "bar"));
}

TEST(test_parse_duration) {
  uint64_t d;

  assert(parse_duration("10s", &d) == 0);
  assert(d == 10 * USEC_PER_SEC);

  assert(parse_duration("5m", &d) == 0);
  assert(d == 5 * USEC_PER_MIN);

  assert(parse_duration("2h", &d) == 0);
  assert(d == 2 * USEC_PER_HOUR);

  assert(parse_duration("1d", &d) == 0);
  assert(d == 1 * USEC_PER_DAY);

  assert(parse_duration("3w", &d) == 0);
  assert(d == 3 * USEC_PER_WEEK);

  assert(parse_duration("500ms", &d) == 0);
  assert(d == 500 * USEC_PER_MSEC);

  assert(parse_duration("500us", &d) == 0);
  assert(d == 500);

  assert(parse_duration("0", &d) == 0);
  assert(d == 0);

  // compound durations (if supported)
  assert(parse_duration("1d2h", &d) == 0);
}

TEST(test_parse_duration_invalid) {
  uint64_t d;
  assert(parse_duration("", &d) < 0);
  assert(parse_duration("abc", &d) < 0);
}

TEST(test_parse_mode) {
  mode_t m;

  assert(parse_mode("0755", &m) == 0);
  assert(m == 0755);

  assert(parse_mode("0644", &m) == 0);
  assert(m == 0644);

  assert(parse_mode("1777", &m) == 0);
  assert(m == 01777);

  assert(parse_mode("0000", &m) == 0);
  assert(m == 0);
}

TEST(test_parse_mode_invalid) {
  mode_t m;
  assert(parse_mode("9999", &m) < 0);
  assert(parse_mode("", &m) < 0);
  assert(parse_mode("abcd", &m) < 0);
}

TEST(test_parse_uid) {
  uid_t u;
  assert(parse_uid("0", &u) == 0);
  assert(u == 0);

  assert(parse_uid("1000", &u) == 0);
  assert(u == 1000);

  assert(parse_uid("", &u) < 0);
  assert(parse_uid("-1", &u) < 0);
}

TEST(test_resolve_user) {
  uid_t u;
  // root should always exist
  assert(resolve_user("root", &u) == 0);
  assert(u == 0);

  // numeric should work
  assert(resolve_user("0", &u) == 0);
  assert(u == 0);

  // nonexistent user
  assert(resolve_user("__nonexistent_user__", &u) < 0);
}

TEST(test_resolve_group) {
  gid_t g;
  // root should always exist
  assert(resolve_group("root", &g) == 0);
  assert(g == 0);
}

TEST(test_cleanup_free) {
  _cleanup_free_ char *s = strdup("cleanup test");
  assert(s != NULL);
}

TEST(test_cleanup_close) {
  _cleanup_close_ int fd = -1;
  (void)fd;
}

TEST(test_safe_close) {
  assert(safe_close(-1) == -1);
  // opening /dev/null and closing it should work
  int fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
  assert(fd >= 0);
  assert(safe_close(fd) == -1);
}

TEST(test_read_file_full) {
  // write a temp file, read it back
  char tmpl[] = "/tmp/seedfiles-test-XXXXXX";
  int fd = mkstemp(tmpl);
  assert(fd >= 0);
  assert(write(fd, "hello", 5) == 5);
  close(fd);

  _cleanup_free_ char *content = NULL;
  size_t sz = 0;
  assert(read_file_full(tmpl, &content, &sz) == 0);
  assert(sz == 5);
  assert(memcmp(content, "hello", 5) == 0);

  unlink(tmpl);
}

TEST(test_rm_rf) {
  // create a temp dir with nested content
  char tmpl[] = "/tmp/seedfiles-test-rm-XXXXXX";
  assert(mkdtemp(tmpl) != NULL);

  _cleanup_free_ char *sub = path_join(tmpl, "sub");
  assert(mkdir(sub, 0755) == 0);

  _cleanup_free_ char *f = path_join(sub, "file.txt");
  int fd = open(f, O_WRONLY | O_CREAT | O_CLOEXEC, 0644);
  assert(fd >= 0);
  assert(write(fd, "data", 4) == 4);
  close(fd);

  struct stat st;
  assert(stat(tmpl, &st) == 0);

  assert(rm_rf(tmpl) == 0);
  assert(stat(tmpl, &st) < 0);
}

TEST(test_get_boot_id) {
  const char *id = get_boot_id();
  // should return something on a running linux system
  if (id) {
    assert(strlen(id) > 0);
    assert(strlen(id) <= 36);
  }
}

TEST(test_get_machine_id) {
  const char *id = get_machine_id();
  if (id) {
    assert(strlen(id) > 0);
    assert(strlen(id) <= 36);
  }
}

int main(void) {
  printf("test-util:\n");
  RUN(test_startswith);
  RUN(test_endswith);
  RUN(test_strstrip);
  RUN(test_xstrdup);
  RUN(test_xstrndup);
  RUN(test_xasprintf);
  RUN(test_cunescape);
  RUN(test_extract_first_word_basic);
  RUN(test_extract_first_word_quotes);
  RUN(test_extract_first_word_cunescape);
  RUN(test_extract_first_word_empty);
  RUN(test_extract_first_word_custom_sep);
  RUN(test_path_is_absolute);
  RUN(test_path_simplify);
  RUN(test_path_join);
  RUN(test_path_startswith);
  RUN(test_path_parent);
  RUN(test_path_basename);
  RUN(test_parse_duration);
  RUN(test_parse_duration_invalid);
  RUN(test_parse_mode);
  RUN(test_parse_mode_invalid);
  RUN(test_parse_uid);
  RUN(test_resolve_user);
  RUN(test_resolve_group);
  RUN(test_cleanup_free);
  RUN(test_cleanup_close);
  RUN(test_safe_close);
  RUN(test_read_file_full);
  RUN(test_rm_rf);
  RUN(test_get_boot_id);
  RUN(test_get_machine_id);
  printf("All tests passed.\n");
  return EXIT_SUCCESS;
}
