/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include "gen-c_glib/t_test_container_test_types.h"
#include "gen-c_glib/t_test_container_service.h"

#include <thrift/c_glib/thrift.h>
#include <thrift/c_glib/protocol/thrift_binary_protocol_factory.h>
#include <thrift/c_glib/protocol/thrift_binary_protocol.h>
#include <thrift/c_glib/protocol/thrift_protocol_factory.h>
#include <thrift/c_glib/server/thrift_server.h>
#include <thrift/c_glib/server/thrift_simple_server.h>
#include <thrift/c_glib/transport/thrift_buffered_transport_factory.h>
#include <thrift/c_glib/transport/thrift_buffered_transport.h>
#include <thrift/c_glib/transport/thrift_server_socket.h>
#include <thrift/c_glib/transport/thrift_server_transport.h>
#include <thrift/c_glib/transport/thrift_socket.h>

#include <glib-object.h>
#include <glib.h>

#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>

#define TEST_SERVER_HOSTNAME "localhost"
#define TEST_SERVER_PORT     9090

/* --------------------------------------------------------------------------
   The ContainerService handler we'll use for testing */

G_BEGIN_DECLS

GType test_container_service_handler_get_type (void);

#define TYPE_TEST_CONTAINER_SERVICE_HANDLER \
  (test_container_service_handler_get_type ())

#define TEST_CONTAINER_SERVICE_HANDLER(obj)                             \
  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                                   \
                               TYPE_TEST_CONTAINER_SERVICE_HANDLER,     \
                               TestContainerServiceHandler))
#define TEST_CONTAINER_SERVICE_HANDLER_CLASS(c)                         \
  (G_TYPE_CHECK_CLASS_CAST ((c),                                        \
                            TYPE_TEST_CONTAINER_SERVICE_HANDLER,        \
                            TestContainerServiceHandlerClass))
#define IS_TEST_CONTAINER_SERVICE_HANDLER(obj)                          \
  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                                   \
                               TYPE_TEST_CONTAINER_SERVICE_HANDLER))
#define IS_TEST_CONTAINER_SERVICE_HANDLER_CLASS(c)                      \
  (G_TYPE_CHECK_CLASS_TYPE ((c),                                        \
                            TYPE_TEST_CONTAINER_SERVICE_HANDLER))
#define TEST_CONTAINER_SERVICE_HANDLER_GET_CLASS(obj)                   \
  (G_TYPE_INSTANCE_GET_CLASS ((obj),                                    \
                              TYPE_TEST_CONTAINER_SERVICE_HANDLER,      \
                              TestContainerServiceHandlerClass))

struct _TestContainerServiceHandler {
  TTestContainerServiceHandler parent_instance;

  /* private */
  GPtrArray *string_list;
};
typedef struct _TestContainerServiceHandler TestContainerServiceHandler;

struct _TestContainerServiceHandlerClass {
  TTestContainerServiceHandlerClass parent_class;
};
typedef struct _TestContainerServiceHandlerClass
  TestContainerServiceHandlerClass;

G_END_DECLS

/* -------------------------------------------------------------------------- */

G_DEFINE_TYPE (TestContainerServiceHandler,
               test_container_service_handler,
               T_TEST_TYPE_CONTAINER_SERVICE_HANDLER)

/* A helper function used to append copies of strings to a string list */
static void append_string_to_ptr_array (gpointer element, gpointer ptr_array)
{
  g_ptr_array_add ((GPtrArray *)ptr_array, g_strdup ((gchar *)element));
}

/* Accept a string list from the client and append its contents to our internal
   list */
static gboolean
test_container_service_handler_receive_string_list (TTestContainerServiceIf *iface,
                                                    const GPtrArray *stringList,
                                                    GError **error)
{
  TestContainerServiceHandler *self = TEST_CONTAINER_SERVICE_HANDLER (iface);

  /* Append the client's strings to our own internal string list */
  g_ptr_array_foreach ((GPtrArray *)stringList,
                       append_string_to_ptr_array,
                       self->string_list);

  g_clear_error (error);
  return TRUE;
}

/* Return the contents of our internal string list to the client */
static gboolean
test_container_service_handler_return_string_list (TTestContainerServiceIf *iface,
                                                   GPtrArray **_return,
                                                   GError **error)
{
  TestContainerServiceHandler *self = TEST_CONTAINER_SERVICE_HANDLER (iface);

  /* Return (copies of) the strings contained in our list */
  g_ptr_array_foreach (self->string_list,
                       append_string_to_ptr_array,
                       *_return);

  g_clear_error (error);
  return TRUE;
}

static gboolean
test_container_service_handler_return_list_string_list (TTestContainerServiceIf *iface,
                                                        GPtrArray **_return,
                                                        GError **error)
{
  TestContainerServiceHandler *self = TEST_CONTAINER_SERVICE_HANDLER (iface);
  GPtrArray *nested_list;

  /* Return a list containing our list of strings */
  nested_list
    = g_ptr_array_new_with_free_func ((GDestroyNotify)g_ptr_array_unref);
  g_ptr_array_add (nested_list, self->string_list);
  g_ptr_array_ref (self->string_list);

  g_ptr_array_add (*_return, nested_list);

  g_clear_error (error);
  return TRUE;
}

static gboolean
test_container_service_handler_return_typedefd_list_string_list (TTestContainerServiceIf *iface,
                                                                 TTestListStringList **_return,
                                                                 GError **error)
{
  TestContainerServiceHandler *self = TEST_CONTAINER_SERVICE_HANDLER (iface);
  TTestStringList *nested_list;

  /* Return a list containing our list of strings */
  nested_list
    = g_ptr_array_new_with_free_func ((GDestroyNotify)g_ptr_array_unref);
  g_ptr_array_add (nested_list, self->string_list);
  g_ptr_array_ref (self->string_list);

  g_ptr_array_add (*_return, nested_list);

  g_clear_error (error);
  return TRUE;
}

static void
test_container_service_handler_finalize (GObject *object) {
  TestContainerServiceHandler *self = TEST_CONTAINER_SERVICE_HANDLER (object);

  /* Destroy our internal containers */
  g_ptr_array_unref (self->string_list);
  self->string_list = NULL;

  G_OBJECT_CLASS (test_container_service_handler_parent_class)->
    finalize (object);
}

static void
test_container_service_handler_init (TestContainerServiceHandler *self)
{
  /* Create our internal containers */
  self->string_list = g_ptr_array_new_with_free_func (g_free);
}

static void
test_container_service_handler_class_init (TestContainerServiceHandlerClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  TTestContainerServiceHandlerClass *parent_class =
    T_TEST_CONTAINER_SERVICE_HANDLER_CLASS (klass);

  gobject_class->finalize = test_container_service_handler_finalize;

  parent_class->receive_string_list =
    test_container_service_handler_receive_string_list;
  parent_class->return_string_list =
    test_container_service_handler_return_string_list;
  parent_class->return_list_string_list =
    test_container_service_handler_return_list_string_list;
  parent_class->return_typedefd_list_string_list =
    test_container_service_handler_return_typedefd_list_string_list;
}

/* -------------------------------------------------------------------------- */

/* Our test server, declared globally so we can access it within a signal
   handler */
ThriftServer *server = NULL;

/* A signal handler used to detect when the child process (the test suite) has
   exited so we know to shut down the server and terminate ourselves */
static void
sigchld_handler (int signal_number)
{
  THRIFT_UNUSED_VAR (signal_number);

  /* The child process (the tests) has exited or been terminated; shut down the
     server gracefully */
  if (server != NULL)
    thrift_server_stop (server);
}

/* A helper function that executes a test case against a newly constructed
   service client */
static void
execute_with_service_client (void (*test_case)(TTestContainerServiceIf *,
                                               GError **))
{
  ThriftSocket *socket;
  ThriftTransport *transport;
  ThriftProtocol *protocol;

  TTestContainerServiceIf *client;

  GError *error = NULL;

  /* Create a client with which to access the server */
  socket    = g_object_new (THRIFT_TYPE_SOCKET,
                            "hostname", TEST_SERVER_HOSTNAME,
                            "port",     TEST_SERVER_PORT,
                            NULL);
  transport = g_object_new (THRIFT_TYPE_BUFFERED_TRANSPORT,
                            "transport", socket,
                            NULL);
  protocol  = g_object_new (THRIFT_TYPE_BINARY_PROTOCOL,
                            "transport", transport,
                            NULL);

  thrift_transport_open (transport, &error);
  g_assert_no_error (error);

  client = g_object_new (T_TEST_TYPE_CONTAINER_SERVICE_CLIENT,
                         "input_protocol",  protocol,
                         "output_protocol", protocol,
                         NULL);

  /* Execute the test against this client */
  (*test_case)(client, &error);
  g_assert_no_error (error);

  /* Clean up and exit */
  thrift_transport_close (transport, NULL);

  g_object_unref (client);
  g_object_unref (protocol);
  g_object_unref (transport);
  g_object_unref (socket);
}

static void
test_containers_with_default_values (void)
{
  TTestContainersWithDefaultValues *default_values;
  GPtrArray *string_list;

  /* Fetch a new ContainersWithDefaultValues struct and its StringList member */
  default_values = g_object_new (T_TEST_TYPE_CONTAINERS_WITH_DEFAULT_VALUES,
                                 NULL);
  g_object_get (default_values,
                "StringList", &string_list,
                NULL);

  /* Make sure the list has been populated with its default values */
  g_assert_cmpint (string_list->len, ==, 2);
  g_assert_cmpstr (((gchar **)string_list->pdata)[0], ==, "Apache");
  g_assert_cmpstr (((gchar **)string_list->pdata)[1], ==, "Thrift");

  g_ptr_array_unref (string_list);
  g_object_unref (default_values);
}

static void
test_container_service_string_list_inner (TTestContainerServiceIf *client,
                                          GError **error)
{
  gchar *test_data[] = { "one", "two", "three" };

  GPtrArray *outgoing_string_list;
  GPtrArray *incoming_string_list;
  guint index;

  g_clear_error (error);

  /* Prepare our test data (our string list to send) */
  outgoing_string_list = g_ptr_array_new ();
  for (index = 0; index < 3; index += 1)
    g_ptr_array_add (outgoing_string_list, &test_data[index]);

  /* Send our data to the server and make sure we get the same data back on
     retrieve */
  g_assert
    (t_test_container_service_client_receive_string_list (client,
                                                          outgoing_string_list,
                                                          error) &&
     *error == NULL);

  incoming_string_list = g_ptr_array_new ();
  g_assert
    (t_test_container_service_client_return_string_list (client,
                                                         &incoming_string_list,
                                                         error) &&
     *error == NULL);

  /* Make sure the two lists are equivalent */
  g_assert_cmpint (incoming_string_list->len, ==, outgoing_string_list->len);
  for (index = 0; index < incoming_string_list->len; index += 1)
    g_assert_cmpstr (((gchar **)incoming_string_list->pdata)[index],
                     ==,
                     ((gchar **)outgoing_string_list->pdata)[index]);

  /* Clean up and exit */
  g_ptr_array_unref (incoming_string_list);
  g_ptr_array_unref (outgoing_string_list);
}

static void
test_container_service_string_list (void)
{
    execute_with_service_client (test_container_service_string_list_inner);
}

static void
test_container_service_list_string_list_inner (TTestContainerServiceIf *client,
                                               GError **error)
{
  GPtrArray *incoming_list;
  GPtrArray *nested_list;

  g_clear_error (error);

  /* Receive a list of string lists from the server */
  incoming_list =
    g_ptr_array_new_with_free_func ((GDestroyNotify)g_ptr_array_unref);
  g_assert
    (t_test_container_service_client_return_list_string_list (client,
                                                              &incoming_list,
                                                              error) &&
     *error == NULL);

  /* Make sure the list and its contents are valid */
  g_assert_cmpint (incoming_list->len, >, 0);

  nested_list = (GPtrArray *)g_ptr_array_index (incoming_list, 0);
  g_assert (nested_list != NULL);
  g_assert_cmpint (nested_list->len, >=, 0);

  /* Clean up and exit */
  g_ptr_array_unref (incoming_list);
}

static void
test_container_service_list_string_list (void)
{
  execute_with_service_client (test_container_service_list_string_list_inner);
}

static void
test_container_service_typedefd_list_string_list_inner (TTestContainerServiceIf *client,
                                                        GError **error)
{
  TTestListStringList *incoming_list;
  TTestStringList *nested_list;

  g_clear_error (error);

  /* Receive a list of string lists from the server */
  incoming_list =
    g_ptr_array_new_with_free_func ((GDestroyNotify)g_ptr_array_unref);
  g_assert
    (t_test_container_service_client_return_list_string_list (client,
                                                              &incoming_list,
                                                              error) &&
     *error == NULL);

  /* Make sure the list and its contents are valid */
  g_assert_cmpint (incoming_list->len, >, 0);

  nested_list = (TTestStringList *)g_ptr_array_index (incoming_list, 0);
  g_assert (nested_list != NULL);
  g_assert_cmpint (nested_list->len, >=, 0);

  /* Clean up and exit */
  g_ptr_array_unref (incoming_list);
}

static void
test_container_service_typedefd_list_string_list (void)
{
  execute_with_service_client
    (test_container_service_typedefd_list_string_list_inner);
}

int
main(int argc, char *argv[])
{
  pid_t pid;
  int status;

#if (!GLIB_CHECK_VERSION (2, 36, 0))
  g_type_init ();
#endif

  /* Fork to run our test suite in a child process */
  pid = fork ();
  g_assert_cmpint (pid, >=, 0);

  if (pid == 0) {    /* The child process */
    /* Wait a moment for the server to finish starting */
    sleep (1);

    g_test_init (&argc, &argv, NULL);

    g_test_add_func
      ("/testcontainertest/ContainerTest/Structs/ContainersWithDefaultValues",
       test_containers_with_default_values);
    g_test_add_func
      ("/testcontainertest/ContainerTest/Services/ContainerService/StringList",
       test_container_service_string_list);
    g_test_add_func
      ("/testcontainertest/ContainerTest/Services/ContainerService/ListStringList",
       test_container_service_list_string_list);
    g_test_add_func
      ("/testcontainertest/ContainerTest/Services/ContainerService/TypedefdListStringList",
       test_container_service_typedefd_list_string_list);

    /* Run the tests and make the result available to our parent process */
    _exit (g_test_run ());
  }
  else {
    TTestContainerServiceHandler *handler;
    TTestContainerServiceProcessor *processor;

    ThriftServerTransport *server_transport;
    ThriftTransportFactory *transport_factory;
    ThriftProtocolFactory *protocol_factory;

    struct sigaction sigchld_action;

    GError *error = NULL;
    int exit_status = 1;

    /* Trap the event of the child process terminating so we know to stop the
       server and exit */
    memset (&sigchld_action, 0, sizeof (sigchld_action));
    sigchld_action.sa_handler = sigchld_handler;
    sigchld_action.sa_flags = SA_RESETHAND;
    sigaction (SIGCHLD, &sigchld_action, NULL);

    /* Create our test server */
    handler = g_object_new (TYPE_TEST_CONTAINER_SERVICE_HANDLER,
                            NULL);
    processor = g_object_new (T_TEST_TYPE_CONTAINER_SERVICE_PROCESSOR,
                              "handler", handler,
                              NULL);
    server_transport = g_object_new (THRIFT_TYPE_SERVER_SOCKET,
                                     "port", TEST_SERVER_PORT,
                                     NULL);
    transport_factory = g_object_new (THRIFT_TYPE_BUFFERED_TRANSPORT_FACTORY,
                                      NULL);
    protocol_factory = g_object_new (THRIFT_TYPE_BINARY_PROTOCOL_FACTORY,
                                     NULL);

    server = g_object_new (THRIFT_TYPE_SIMPLE_SERVER,
                           "processor",                processor,
                           "server_transport",         server_transport,
                           "input_transport_factory",  transport_factory,
                           "output_transport_factory", transport_factory,
                           "input_protocol_factory",   protocol_factory,
                           "output_protocol_factory",  protocol_factory,
                           NULL);

    /* Start the server */
    thrift_server_serve (server, &error);

    /* Make sure the server stopped only because it was interrupted (by the
       child process terminating) */
    g_assert(!error || g_error_matches(error,
                                       THRIFT_SERVER_SOCKET_ERROR,
                                       THRIFT_SERVER_SOCKET_ERROR_ACCEPT));

    /* Free our resources */
    g_clear_object (&server);
    g_clear_object (&protocol_factory);
    g_clear_object (&transport_factory);
    g_clear_object (&server_transport);
    g_clear_object (&processor);
    g_clear_object (&handler);

    /* Wait for the child process to complete and return its exit status */
    g_assert (wait (&status) == pid);
    if (WIFEXITED (status))
      exit_status = WEXITSTATUS (status);

    return exit_status;
  }
}
