/*
 * Copyright © 2021 Collabora Ltd.
 *
 * SPDX-License-Identifier: MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <steam-runtime-tools/steam-runtime-tools.h>
#include <steam-runtime-tools/glib-backports-internal.h>
#include <steam-runtime-tools/utils-internal.h>

#include <glib.h>

#include <fcntl.h>
#include <string.h>
#include <sysexits.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "test-utils.h"

static const char *argv0;
static const char *empty_temp_dir;

typedef struct
{
  TestsOpenFdSet old_fds;
  gchar *srcdir;
  gchar *builddir;
} Fixture;

typedef struct
{
  int unused;
} Config;

typedef struct
{
  const gchar *path;
  const gchar *abi;
} LibInfo;

typedef struct
{
  const gchar *argv[5];
  int exit_status;
  const gchar *stdout_contains;
  const gchar *stderr_contains;
  gboolean version_dependent;
} IdentifyLibraryAbi;

static void
setup (Fixture *f,
       gconstpointer context)
{
  G_GNUC_UNUSED const Config *config = context;

  f->old_fds = tests_check_fd_leaks_enter ();

  /* For the tests we currently have they are not used yet */
  f->srcdir = g_strdup (g_getenv ("G_TEST_SRCDIR"));
  f->builddir = g_strdup (g_getenv ("G_TEST_BUILDDIR"));

  if (f->srcdir == NULL)
    f->srcdir = g_path_get_dirname (argv0);

  if (f->builddir == NULL)
    f->builddir = g_path_get_dirname (argv0);
}

static void
teardown (Fixture *f,
          gconstpointer context)
{
  G_GNUC_UNUSED const Config *config = context;

  g_free (f->srcdir);
  g_free (f->builddir);

  tests_check_fd_leaks_leave (f->old_fds);
}

static void
_spawn_and_check_output (const IdentifyLibraryAbi *t)
{
  g_autoptr(GString) command = g_string_new ("");
  g_autofree gchar *child_stdout = NULL;
  g_autofree gchar *child_stderr = NULL;
  g_autoptr(GError) error = NULL;
  gboolean ret;
  int wait_status = -1;
  size_t i;

  for (i = 0; t->argv[i] != NULL; i++)
    {
      g_autofree gchar *quoted = NULL;

      if (i > 0)
        g_string_append (command, " ");

      quoted = g_shell_quote (t->argv[i]);
      g_string_append (command, quoted);
    }

  g_test_message ("%s", command->str);
  ret = g_spawn_sync (NULL,    /* working directory */
                      (gchar **) t->argv,
                      NULL,    /* envp */
                      G_SPAWN_SEARCH_PATH,
                      NULL,    /* child setup */
                      NULL,    /* user data */
                      &child_stdout,
                      &child_stderr,
                      &wait_status,
                      &error);
  g_assert_no_error (error);
  g_assert_true (ret);
  g_test_message ("stdout: %s", child_stdout);
  g_test_message ("stderr: %s", child_stderr);
  g_assert_true (WIFEXITED (wait_status));
  g_assert_cmpint (WEXITSTATUS (wait_status), ==, t->exit_status);
  g_assert_nonnull (child_stdout);
  g_assert_true (g_utf8_validate (child_stdout, -1, NULL));
  g_assert_nonnull (child_stderr);
  g_assert_true (g_utf8_validate (child_stderr, -1, NULL));

  if (t->version_dependent && g_getenv ("SRT_TEST_UNINSTALLED") == NULL)
    {
      g_debug ("Not testing --version match in installed-tests");
    }
  else if (t->stdout_contains != NULL)
    {
      g_assert_cmpstr (strstr (child_stdout, t->stdout_contains), !=, NULL);
    }

  if (t->stderr_contains != NULL)
    g_assert_cmpstr (strstr (child_stderr, t->stderr_contains), !=, NULL);
}

static void
test_arguments_validation (Fixture *f,
                           gconstpointer context)
{
  const IdentifyLibraryAbi identify_lib_abi[] =
  {
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--ldconfig",
        NULL,
      },
      .exit_status = 0,
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--ldconfig",
        "--print0",
        NULL,
      },
      .exit_status = 0,
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--directory",
        empty_temp_dir,
        NULL,
      },
      .exit_status = 0,
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--this-option-is-unsupported",
        NULL,
      },
      .exit_status = EX_USAGE,
      .stderr_contains = "Unknown option",
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "this-argument-is-unsupported",
        NULL,
      },
      .exit_status = EX_USAGE,
      .stderr_contains = "Exactly one of",
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--ldconfig",
        "--directory",
        empty_temp_dir,
        NULL,
      },
      .exit_status = EX_USAGE,
      .stderr_contains = "Exactly one of",
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--ldconfig",
        "--ldconfig-paths",
        empty_temp_dir,
        NULL,
      },
      .exit_status = EX_USAGE,
      .stderr_contains = "Exactly one of",
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--ldconfig-paths",
        "--directory",
        empty_temp_dir,
        NULL,
      },
      .exit_status = EX_USAGE,
      .stderr_contains = "Exactly one of",
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        NULL,
      },
      .exit_status = EX_USAGE,
      .stderr_contains = "Exactly one of",
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--ldconfig",
        "--one-line",
      },
      .exit_status = EX_USAGE,
      .stderr_contains = "--one-line only works with --ldconfig-paths",
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--ldconfig-paths",
        "--one-line",
        "--print0",
      },
      .exit_status = EX_USAGE,
      .stderr_contains = "--one-line is not compatible with --print0",
    },
    {
      .argv =
      {
        "steam-runtime-identify-library-abi",
        "--directory",
        "/this_directory_does_not_exist",
        NULL,
      },
      .exit_status = 1,
      .stderr_contains = "Unable to find real path",
    },
  };

  for (gsize i = 0; i < G_N_ELEMENTS (identify_lib_abi); i++)
    _spawn_and_check_output (&identify_lib_abi[i]);
}

/*
 * Test `steam-runtime-identify-library-abi --help` and `--version`.
 */
static void
test_help_and_version (Fixture *f,
                       gconstpointer context)
{
  const IdentifyLibraryAbi identify_lib_abi[] =
  {
    {
      .argv =
      {
        "env",
        "LC_ALL=C",
        "steam-runtime-identify-library-abi",
        "--version",
        NULL,
      },
      .exit_status = 0,
      .stdout_contains = VERSION,
      .version_dependent = TRUE,
    },
    {
      .argv =
      {
        "env",
        "LC_ALL=C",
        "steam-runtime-identify-library-abi",
        "--help",
        NULL,
      },
      .exit_status = 0,
      .stdout_contains = "OPTION",
    },
  };

  for (gsize i = 0; i < G_N_ELEMENTS (identify_lib_abi); i++)
    _spawn_and_check_output (&identify_lib_abi[i]);
}

static void
test_library_identification (Fixture *f,
                             gconstpointer context)
{
  gboolean ret;
  int exit_status = -1;
  GError *error = NULL;
  gchar *child_stdout = NULL;
  gchar *child_stderr = NULL;
  gsize i;
  const gchar *argv[] =
  {
    "steam-runtime-identify-library-abi",
    "--ldconfig",
    NULL,
    NULL,
  };

  ret = g_spawn_sync (NULL,    /* working directory */
                      (gchar **) argv,
                      NULL,    /* envp */
                      G_SPAWN_SEARCH_PATH,
                      NULL,    /* child setup */
                      NULL,    /* user data */
                      &child_stdout,
                      &child_stderr,
                      &exit_status,
                      &error);
  g_assert_no_error (error);
  g_assert_true (ret);
  g_assert_cmpint (exit_status, ==, 0);
  g_assert_nonnull (child_stdout);
  g_assert_cmpstr (child_stdout, !=, "");
  g_assert_true (g_utf8_validate (child_stdout, -1, NULL));
  g_assert_nonnull (child_stderr);

  const LibInfo glib_info[] =
  {
    {
      .path = "/usr/lib/x86_64-linux-gnu/libglib-2.0.so.0",
      .abi = "x86_64-linux-gnu",
    },
    {
      .path = "/lib/x86_64-linux-gnu/libglib-2.0.so.0",
      .abi = "x86_64-linux-gnu",
    },
    {
      .path = "/usr/lib/i386-linux-gnu/libglib-2.0.so.0",
      .abi = "i386-linux-gnu",
    },
    {
      .path = "/lib/i386-linux-gnu/libglib-2.0.so.0",
      .abi = "i386-linux-gnu",
    },
  };

  for (i = 0; i < G_N_ELEMENTS (glib_info); i++)
    {
      g_autofree gchar *expected_out_line = NULL;
      gchar *out_line = strstr (child_stdout, glib_info[i].path);
      if (out_line != NULL)
        {
          gchar *end_of_line = strstr (out_line, "\n");
          g_assert_nonnull (end_of_line);
          end_of_line[0] = '\0';
          expected_out_line = g_strdup_printf ("%s=%s", glib_info[i].path, glib_info[i].abi);
          g_assert_cmpstr (out_line, ==, expected_out_line);
          end_of_line[0] = '\n';
        }
      else
        {
          g_test_message ("\"%s\" seems to not be available in ldconfig output, "
                          "skipping this part of the test", glib_info[i].path);
        }
    }

  g_free (child_stdout);
  g_free (child_stderr);

  argv[1] = "--directory";
  for (i = 0; i < G_N_ELEMENTS (glib_info); i++)
    {
      g_autofree gchar *glib_dirname = NULL;
      g_autofree gchar *expected_out_line = NULL;
      const gchar *out_line;
      gchar *end_of_line;  /* not owned */

      if (!g_file_test (glib_info[i].path, G_FILE_TEST_EXISTS))
        {
          g_test_message ("\"%s\" is not available in the filesystem, skipping this "
                          "part of the test", glib_info[i].path);
          continue;
        }

      glib_dirname = g_path_get_dirname (glib_info[i].path);
      argv[2] = glib_dirname;

      g_test_message ("Running: %s %s %s", argv[0], argv[1], argv[2]);
      ret = g_spawn_sync (NULL,    /* working directory */
                          (gchar **) argv,
                          NULL,    /* envp */
                          G_SPAWN_SEARCH_PATH,
                          NULL,    /* child setup */
                          NULL,    /* user data */
                          &child_stdout,
                          &child_stderr,
                          &exit_status,
                          &error);
      g_assert_no_error (error);
      g_assert_true (ret);
      g_test_message ("stdout: '''\\\n%s'''", child_stdout);
      g_test_message ("stderr: '''\\\n%s'''", child_stderr);
      g_assert_cmpint (exit_status, ==, 0);
      g_assert_nonnull (child_stdout);
      g_assert_cmpstr (child_stdout, !=, "");
      g_assert_true (g_utf8_validate (child_stdout, -1, NULL));
      g_assert_nonnull (child_stderr);

      g_test_message ("Looking for %s in output", glib_info[i].path);
      out_line = strstr (child_stdout, glib_info[i].path);
      g_assert_nonnull (out_line);
      end_of_line = strstr (out_line, "\n");
      g_assert_nonnull (end_of_line);
      end_of_line[0] = '\0';
      expected_out_line = g_strdup_printf ("%s=%s", glib_info[i].path, glib_info[i].abi);
      g_assert_cmpstr (out_line, ==, expected_out_line);
      end_of_line[0] = '\n';

      g_free (child_stdout);
      g_free (child_stderr);
    }
}

int
main (int argc,
      char **argv)
{
  g_auto(GLnxTmpDir) temp_dir = { .initialized = FALSE };
  g_autoptr(GError) error = NULL;

  argv0 = argv[0];

  _srt_tests_init (&argc, &argv, NULL);

  /* Creates an empty temporary directory to test the --directory option */
  glnx_mkdtemp ("empty-dir-XXXXXX", 0700, &temp_dir, &error);
  g_assert_no_error (error);
  empty_temp_dir = temp_dir.path;

  g_test_add ("/identify-library-abi-cli/arguments_validation", Fixture, NULL,
              setup, test_arguments_validation, teardown);
  g_test_add ("/identify-library-abi-cli/help-and-version", Fixture, NULL,
              setup, test_help_and_version, teardown);
  g_test_add ("/identify-library-abi-cli/library-identification", Fixture, NULL,
              setup, test_library_identification, teardown);

  return g_test_run ();
}
