// Copyright 2012 Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
// * Neither the name of Google Inc. nor the names of its contributors
//   may be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#if defined(HAVE_CONFIG_H)
#include "config.h"
#endif

#include "cli.h"

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "defs.h"
#include "error.h"
#include "run.h"

#if !defined(GID_MAX)
#   define GID_MAX INT_MAX
#endif
#if !defined(UID_MAX)
#   define UID_MAX INT_MAX
#endif

#if defined(HAVE_GETOPT_GNU)
#   define GETOPT_PLUS "+"
#else
#   define GETOPT_PLUS
#endif


/// Terminates execution if the given error is set.
///
/// \param error The error to validate.
///
/// \post The program terminates if the given error is set.
static void
check_error(kyua_error_t error)
{
   if (kyua_error_is_set(error)) {
       const bool usage_error = kyua_error_is_type(error,
                                                   kyua_usage_error_type);

       char buffer[1024];
       kyua_error_format(error, buffer, sizeof(buffer));
       kyua_error_free(error);

       errx(usage_error ? EXIT_USAGE_ERROR : EXIT_INTERNAL_ERROR,
            "%s", buffer);
   }
}


/// Converts a string to an unsigned long.
///
/// \param str The string to convert.
/// \param message Part of the error message to print if the string does not
///     represent a valid unsigned long number.
/// \param max Maximum accepted value.
///
/// \return The converted numerical value.
///
/// \post The program terminates if the value is invalid.
static unsigned long
parse_ulong(const char* str, const char* message, const unsigned long max)
{
   char *endptr;

   errno = 0;
   const unsigned long value = strtoul(str, &endptr, 10);
   if (str[0] == '\0' || *endptr != '\0')
       errx(EXIT_USAGE_ERROR, "%s '%s' (not a number)", message, str);
   else if (errno == ERANGE || value == LONG_MAX || value > max)
       errx(EXIT_USAGE_ERROR, "%s '%s' (out of range)", message, str);
   return value;
}


/// Clears getopt(3) state to allow calling the function again.
static void
reset_getopt(void)
{
   opterr = 0;
   optind = GETOPT_OPTIND_RESET_VALUE;
#if defined(HAVE_GETOPT_WITH_OPTRESET)
   optreset = 1;
#endif
}


/// Prints the list of test cases and their metadata in a test program.
///
/// \param argc Number of arguments to the command, including the command name.
/// \param argv Arguments to the command, including the command name.
/// \param tester Description of the tester implemented by this binary.
/// \param run_params Execution parameters to configure the test process.
///
/// \return An exit status to indicate the success or failure of the listing.
///
/// \post Usage errors terminate the execution of the program right away.
static int
list_command(const int argc, char* const* const argv,
            const kyua_cli_tester_t* tester,
            const kyua_run_params_t* run_params)
{
   if (argc < 2)
       errx(EXIT_USAGE_ERROR, "No test program provided");
   else if (argc > 2)
       errx(EXIT_USAGE_ERROR, "Only one test program allowed");
   const char* test_program = argv[1];

   check_error(tester->list_test_cases(test_program, run_params));
   return EXIT_SUCCESS;
}


/// Runs and cleans up a single test case.
///
/// \param argc Number of arguments to the command, including the command name.
/// \param argv Arguments to the command, including the command name.
/// \param tester Description of the tester implemented by this binary.
/// \param run_params Execution parameters to configure the test process.
///
/// \return An exit status to indicate the success or failure of the test case
/// execution.
///
/// \post Usage errors terminate the execution of the program right away.
static int
test_command(int argc, char* const* argv, const kyua_cli_tester_t* tester,
            const kyua_run_params_t* run_params)
{
#   define MAX_USER_VARIABLES 256
   const char* user_variables[MAX_USER_VARIABLES];

   const char** last_variable = user_variables;
   int ch;
   while ((ch = getopt(argc, argv, GETOPT_PLUS":v:")) != -1) {
       switch (ch) {
       case 'v':
           *last_variable++ = optarg;
           break;

       case ':':
           errx(EXIT_USAGE_ERROR, "%s's -%c requires an argument", argv[0],
                optopt);

       case '?':
           errx(EXIT_USAGE_ERROR, "Unknown %s option -%c", argv[0], optopt);

       default:
           assert(false);
       }
   }
   argc -= optind;
   argv += optind;
   *last_variable = NULL;

   if (argc != 3)
       errx(EXIT_USAGE_ERROR, "Must provide a test program, a test case name "
            "and a result file");
   const char* test_program = argv[0];
   const char* test_case = argv[1];
   const char* result_file = argv[2];

   bool success;
   check_error(tester->run_test_case(test_program, test_case, result_file,
                                     user_variables, run_params, &success));
   return success ? EXIT_SUCCESS : EXIT_FAILURE;
}


/// Generic entry point to the tester's command-line interface.
///
/// \param argc Verbatim argc passed to main().
/// \param argv Verbatim argv passed to main().
/// \param tester Description of the tester implemented by this binary.
///
/// \return An exit status.
///
/// \post Usage errors terminate the execution of the program right away.
int
kyua_cli_main(int argc, char* const* argv, const kyua_cli_tester_t* tester)
{
   kyua_run_params_t run_params;
   kyua_run_params_init(&run_params);

   int ch;
   while ((ch = getopt(argc, argv, GETOPT_PLUS":g:t:u:")) != -1) {
       switch (ch) {
       case 'g':
           run_params.unprivileged_group = (uid_t)parse_ulong(
               optarg, "Invalid GID", GID_MAX);
           break;

       case 't':
           run_params.timeout_seconds = parse_ulong(
               optarg, "Invalid timeout value", LONG_MAX);
           break;

       case 'u':
           run_params.unprivileged_user = (uid_t)parse_ulong(
               optarg, "Invalid UID", UID_MAX);
           break;

       case ':':
           errx(EXIT_USAGE_ERROR, "-%c requires an argument", optopt);

       case '?':
           errx(EXIT_USAGE_ERROR, "Unknown option -%c", optopt);

       default:
           assert(false);
       }
   }
   argc -= optind;
   argv += optind;
   reset_getopt();

   if (argc == 0)
       errx(EXIT_USAGE_ERROR, "Must provide a command");
   const char* command = argv[0];

   // Keep sorted by order of likelyhood (yeah, micro-optimization).
   if (strcmp(command, "test") == 0)
       return test_command(argc, argv, tester, &run_params);
   else if (strcmp(command, "list") == 0)
       return list_command(argc, argv, tester, &run_params);
   else
       errx(EXIT_USAGE_ERROR, "Unknown command '%s'", command);
}