/*
 * Copyright (C) 2013 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "cockpitauth.h"
#include "cockpitws.h"

#include "common/cockpitconf.h"
#include "cockpiterror.h"
#include "cockpitwebrequest-private.h"

#include "websocket.h"

#include "testlib/cockpittest.h"
#include "testlib/mock-auth.h"

#include <string.h>

/* Mock override these from other files */
extern const gchar *cockpit_config_file;
extern const gchar *cockpit_ws_max_startups;

typedef struct {
  CockpitAuth *auth;
} Test;

static void
setup (Test *test,
       gconstpointer data)
{
  test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
}

static void
teardown (Test *test,
          gconstpointer data)
{
  g_object_unref (test->auth);
}

static void
setup_normal (Test *test,
              gconstpointer data)
{
  cockpit_config_file = SRCDIR "/src/common/mock-config/cockpit/cockpit.conf";
  test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
}

static void
setup_alt_config (Test *test,
              gconstpointer data)
{
  cockpit_config_file = SRCDIR "/src/common/mock-config/cockpit/cockpit-alt.conf";
  test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
}

static void
teardown_normal (Test *test,
                 gconstpointer data)
{
  cockpit_assert_expected ();
  g_object_unref (test->auth);
  cockpit_conf_cleanup ();
}

static void
on_ready_get_result (GObject *source,
                     GAsyncResult *result,
                     gpointer user_data)
{
  GAsyncResult **retval = user_data;
  g_assert (retval != NULL);
  g_assert (*retval == NULL);
  *retval = g_object_ref (result);
}

static void
test_application (Test *test,
                  gconstpointer data)
{
  gchar *application = NULL;
  gboolean is_host = FALSE;

  application = cockpit_auth_parse_application ("/", &is_host);
  g_assert_cmpstr ("cockpit", ==, application);
  g_assert_false (is_host);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/=", &is_host);
  g_assert_cmpstr ("cockpit", ==, application);
  g_assert_false (is_host);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/other/other", &is_host);
  g_assert_cmpstr ("cockpit", ==, application);
  g_assert_false (is_host);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/=other/other", &is_host);
  g_assert_true (is_host);
  g_assert_cmpstr ("cockpit+=other", ==, application);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/=other", &is_host);
  g_assert_true (is_host);
  g_assert_cmpstr ("cockpit+=other", ==, application);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/=other/", &is_host);
  g_assert_true (is_host);
  g_assert_cmpstr ("cockpit+=other", ==, application);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/cockpit", &is_host);
  g_assert_cmpstr ("cockpit", ==, application);
  g_assert_false (is_host);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/cockpit/login", &is_host);
  g_assert_cmpstr ("cockpit", ==, application);
  g_assert_false (is_host);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/cockpit+application", &is_host);
  g_assert_cmpstr ("cockpit+application", ==, application);
  g_assert_false (is_host);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/cockpit+application/", &is_host);
  g_assert_false (is_host);
  g_assert_cmpstr ("cockpit+application", ==, application);
  g_clear_pointer (&application, g_free);

  application = cockpit_auth_parse_application ("/cockpit+application/other/other", &is_host);
  g_assert_false (is_host);
  g_assert_cmpstr ("cockpit+application", ==, application);
  g_clear_pointer (&application, g_free);
}

typedef struct {
  const gchar *superuser_mode;
  gboolean expect_stored_password;
} UserpassFixture;

static const UserpassFixture fixture_superuser_any = {
  .superuser_mode = "any",
  .expect_stored_password = TRUE
};

static void
test_userpass_cookie_check (Test *test,
                            gconstpointer data)
{
  const UserpassFixture *fix = data;
  GAsyncResult *result = NULL;
  CockpitWebService *service;
  CockpitWebService *prev_service;
  CockpitCreds *creds;
  CockpitCreds *prev_creds;
  JsonObject *response = NULL;
  GError *error = NULL;
  GHashTable *headers;

  headers = mock_auth_basic_header ("me", "this is the password");
  if (fix && fix->superuser_mode)
    g_hash_table_insert (headers, g_strdup ("X-Superuser"), g_strdup (fix->superuser_mode));
  cockpit_auth_login_async (test->auth,
                            WebRequest(.path = "/cockpit/", .headers = headers),
                            on_ready_get_result, &result);
  g_hash_table_unref (headers);

  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);

  headers = web_socket_util_new_headers ();
  response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
  g_assert_no_error (error);

  /* Get the service */
  mock_auth_include_cookie_as_if_client (headers, headers, "cockpit");
  service = cockpit_auth_check_cookie (test->auth,
                                       WebRequest(.path="/cockpit", .headers=headers));

  g_object_unref (result);
  g_assert (service != NULL);
  g_assert (response != NULL);

  creds = cockpit_web_service_get_creds (service);
  g_assert_cmpstr ("me", ==, cockpit_creds_get_user (creds));
  g_assert_cmpstr ("cockpit", ==, cockpit_creds_get_application (creds));
  if (fix && fix->expect_stored_password)
    g_assert_cmpstr ("this is the password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
  else
    g_assert_null (cockpit_creds_get_password (creds));

  prev_service = service;
  g_object_unref (service);
  service = NULL;

  prev_creds = creds;
  creds = NULL;

  mock_auth_include_cookie_as_if_client (headers, headers, "cockpit");

  service = cockpit_auth_check_cookie (test->auth,
                                       WebRequest(.path="/cockpit", .headers=headers));
  g_assert (prev_service == service);

  creds = cockpit_web_service_get_creds (service);
  g_assert (prev_creds == creds);

  g_assert_cmpstr ("me", ==, cockpit_creds_get_user (creds));
  if (fix && fix->expect_stored_password)
    g_assert_cmpstr ("this is the password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
  else
    g_assert_null (cockpit_creds_get_password (creds));

  g_hash_table_destroy (headers);
  g_object_unref (service);
  json_object_unref (response);
}

static void
test_userpass_bad (Test *test,
                   gconstpointer data)
{
  GAsyncResult *result = NULL;
  GError *error = NULL;
  JsonObject *response;
  GHashTable *headers;

  headers = mock_auth_basic_header ("me", "bad");
  cockpit_auth_login_async (test->auth,
                            WebRequest(.path="/cockpit", .headers=headers),
                            on_ready_get_result, &result);
  g_hash_table_unref (headers);

  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);

  headers = web_socket_util_new_headers ();
  response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
  g_object_unref (result);

  g_assert (response != NULL);
  g_assert_cmpstr (json_object_get_string_member (response, "problem"), ==, "authentication-failed");
  g_clear_pointer (&response, json_object_unref);

  g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
  g_clear_error (&error);

  g_hash_table_destroy (headers);
}

static void
test_userpass_emptypass (Test *test,
                         gconstpointer data)
{
  GAsyncResult *result = NULL;
  JsonObject *response;
  GError *error = NULL;
  GHashTable *headers;

  headers = mock_auth_basic_header ("aaaaaa", "");
  cockpit_auth_login_async (test->auth,
                            WebRequest(.path="/cockpit", .headers=headers),
                            on_ready_get_result, &result);
  g_hash_table_unref (headers);

  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);

  headers = web_socket_util_new_headers ();
  response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
  g_object_unref (result);

  g_assert (response != NULL);
  g_assert_cmpstr (json_object_get_string_member (response, "problem"), ==, "authentication-failed");
  g_clear_pointer (&response, json_object_unref);

  g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
  g_clear_error (&error);

  g_hash_table_destroy (headers);
}

static void
test_headers_bad (Test *test,
                  gconstpointer data)
{
  GHashTable *headers;

  headers = web_socket_util_new_headers ();

  /* Bad version */
  g_hash_table_insert (headers, g_strdup ("Cookie"), g_strdup ("CockpitAuth=v=1;k=blah"));
  if (cockpit_auth_check_cookie (test->auth,
                                 WebRequest(.path="/cockpit", .headers=headers)))
      g_assert_not_reached ();

  /* Bad hash */
  g_hash_table_remove_all (headers);
  g_hash_table_insert (headers, g_strdup ("Cookie"), g_strdup ("CockpitAuth=v=2;k=blah"));
  if (cockpit_auth_check_cookie (test->auth,
                                 WebRequest(.path="/cockpit", .headers=headers)))
      g_assert_not_reached ();

  /* Bad encoding */
  g_hash_table_remove_all (headers);
  g_hash_table_insert (headers, g_strdup ("Cookie"), g_strdup ("cockpit=d"));
  if (cockpit_auth_check_cookie (test->auth,
                                 WebRequest(.path="/cockpit", .headers=headers)))
      g_assert_not_reached ();

  g_hash_table_destroy (headers);
}

static gboolean
on_timeout_set_flag (gpointer data)
{
  gboolean *flag = data;
  g_assert (*flag == FALSE);
  *flag = TRUE;
  return FALSE;
}

static gboolean
on_idling_set_flag (CockpitAuth *auth,
                    gpointer data)
{
  gboolean *flag = data;
  *flag = TRUE;
  return FALSE;
}

static void
test_idle_timeout (Test *test,
                   gconstpointer data)
{
  GAsyncResult *result = NULL;
  CockpitWebService *service;
  JsonObject *login_response;
  GError *error = NULL;
  GHashTable *headers;
  gboolean flag = FALSE;
  gboolean idling = FALSE;

  /* The idle timeout is one second */
  g_assert (cockpit_ws_service_idle == 1);

  headers = mock_auth_basic_header ("me", "this is the password");
  cockpit_auth_login_async (test->auth,
                            WebRequest(.path="/cockpit", .headers=headers),
                            on_ready_get_result, &result);
  g_hash_table_unref (headers);

  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);

  headers = web_socket_util_new_headers ();
  login_response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
  g_assert (login_response != NULL);
  json_object_unref (login_response);
  g_object_unref (result);
  g_assert_no_error (error);

  /* Logged in ... the webservice is idle though */
  mock_auth_include_cookie_as_if_client (headers, headers, "cockpit");
  service = cockpit_auth_check_cookie (test->auth, WebRequest(.path="/cockpit", .headers=headers));
  g_assert (service != NULL);
  g_assert (cockpit_web_service_get_idling (service));
  g_object_unref (service);

  g_signal_connect (test->auth, "idling", G_CALLBACK (on_idling_set_flag), &idling);

  /* Now wait for 2 seconds, and the service should be gone */
  g_timeout_add_seconds (2, on_timeout_set_flag, &flag);
  while (!flag)
    g_main_context_iteration (NULL, TRUE);

  /* Timeout, no longer logged in */
  service = cockpit_auth_check_cookie (test->auth, WebRequest(.path="/cockpit", .headers=headers));
  g_assert (service == NULL);

  /* Now wait for 3 seconds, and the auth should have said its idling */
  flag = FALSE;
  g_timeout_add_seconds (3, on_timeout_set_flag, &flag);
  while (!flag)
    g_main_context_iteration (NULL, TRUE);

  g_assert (idling == TRUE);
  g_hash_table_destroy (headers);
}

static void
test_process_timeout (Test *test,
                      gconstpointer data)
{
  gboolean idling = FALSE;

  g_signal_connect (test->auth, "idling", G_CALLBACK (on_idling_set_flag), &idling);

  while (!idling)
    g_main_context_iteration (NULL, TRUE);
}

static void
test_max_startups (Test *test,
                   gconstpointer data)
{
  GAsyncResult *result1 = NULL;
  GAsyncResult *result2 = NULL;
  GAsyncResult *result3 = NULL;

  JsonObject *response;

  GHashTable *headers_slow;
  GHashTable *headers_fail;

  GError *error1 = NULL;
  GError *error2 = NULL;
  GError *error3 = NULL;

  cockpit_expect_message ("Request dropped; too many startup connections: 2");

  headers_slow = web_socket_util_new_headers ();
  headers_fail = web_socket_util_new_headers ();
  g_hash_table_insert (headers_slow, g_strdup ("Authorization"), g_strdup ("testscheme failslow"));
  g_hash_table_insert (headers_fail, g_strdup ("Authorization"), g_strdup ("testscheme fail"));

  /* Slow request that takes a while to complete */
  cockpit_auth_login_async (test->auth,
                            WebRequest(.path="/cockpit", .headers=headers_slow),
                            on_ready_get_result, &result1);

  /* Request that gets dropped */
  cockpit_auth_login_async (test->auth,
                            WebRequest(.path="/cockpit", .headers=headers_fail),
                            on_ready_get_result, &result2);
  while (result2 == NULL)
    g_main_context_iteration (NULL, TRUE);
  response = cockpit_auth_login_finish (test->auth, result2, NULL, NULL, &error2);
  g_object_unref (result2);
  g_assert (response == NULL);
  g_assert_cmpstr ("Connection closed by host", ==, error2->message);

  /* Wait for first request to finish */
  while (result1 == NULL)
    g_main_context_iteration (NULL, TRUE);
  response = cockpit_auth_login_finish (test->auth, result1, NULL, NULL, &error1);
  g_object_unref (result1);
  g_assert (response != NULL);
  g_assert_cmpstr (json_object_get_string_member (response, "problem"), ==, "authentication-failed");
  g_assert_cmpstr ("Authentication failed", ==, error1->message);
  g_clear_pointer (&response, json_object_unref);

  /* Now that first is finished we can successfully run another one */
  g_hash_table_insert (headers_fail, g_strdup ("Authorization"), g_strdup ("testscheme fail"));
  cockpit_auth_login_async (test->auth,
                            WebRequest(.path="/cockpit", .headers=headers_fail),
                            on_ready_get_result, &result3);
  while (result3 == NULL)
    g_main_context_iteration (NULL, TRUE);
  response = cockpit_auth_login_finish (test->auth, result3, NULL, NULL, &error3);
  g_object_unref (result3);
  g_assert (response != NULL);
  g_assert_cmpstr (json_object_get_string_member (response, "problem"), ==, "authentication-failed");
  g_clear_pointer (&response, json_object_unref);
  g_assert_cmpstr ("Authentication failed", ==, error3->message);

  g_clear_error (&error1);
  g_clear_error (&error2);
  g_clear_error (&error3);

  g_hash_table_destroy (headers_fail);
  g_hash_table_destroy (headers_slow);
}

typedef struct {
  const gchar *header;
  const gchar *error_message;
  const gchar *warning;
  const gchar *path;
  const gchar *init_problem;
  int error_code;
} ErrorFixture;

typedef struct {
  const gchar *data;
  const gchar *warning;
  const gchar *header;
  const gchar *path;
  const gchar *user;
  const gchar *application;
  const gchar *cookie_name;
  gboolean     expect_stored_password;
} SuccessFixture;

static void
test_custom_fail (Test *test,
                  gconstpointer data)
{
  GAsyncResult *result = NULL;
  JsonObject *response;
  GError *error = NULL;
  GHashTable *headers;
  const ErrorFixture *fix = data;
  const gchar *path = fix->path ? fix->path : "/cockpit";

  if (fix->warning)
    cockpit_expect_warning (fix->warning);

  headers = web_socket_util_new_headers ();
  g_hash_table_insert (headers, g_strdup ("Authorization"), g_strdup (fix->header));

  cockpit_auth_login_async (test->auth, WebRequest(.path=path, .headers=headers), on_ready_get_result, &result);
  g_hash_table_unref (headers);

  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);

  headers = web_socket_util_new_headers ();
  response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
  g_object_unref (result);

  if (fix->init_problem)
    {
      g_assert (response != NULL);
      g_assert_cmpstr (json_object_get_string_member (response, "problem"), ==, fix->init_problem);
      g_clear_pointer (&response, json_object_unref);
    }
  else
    {
      g_assert (response == NULL);
    }

  if (fix->error_code)
    g_assert_error (error, COCKPIT_ERROR, fix->error_code);
  else
    g_assert (error != NULL);

  g_assert_cmpstr (fix->error_message, ==, error->message);
  g_clear_error (&error);

  g_hash_table_destroy (headers);
}

static void
test_custom_timeout (Test *test,
                     gconstpointer data)
{
  cockpit_expect_message ("*session timed out*");
  test_custom_fail (test, data);
}

static void
test_bad_command (Test *test,
                  gconstpointer data)
{
  cockpit_expect_possible_log ("cockpit-protocol", G_LOG_LEVEL_WARNING,
                               "*couldn't recv*");
  cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_WARNING,
                               "*Auth pipe closed: internal-error*");
  cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_WARNING,
                               "*Auth pipe closed: not-found*");
  cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_WARNING,
                               "*Auth pipe closed: terminated*");
  cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_WARNING,
                               "*couldn't write: Connection refused*");
  cockpit_expect_possible_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE,
                               "*couldn't write: Connection refused*");
  cockpit_expect_possible_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE,
                               "*couldn't send: Connection refused*");
  test_custom_fail (test, data);
}

static void
test_custom_success (Test *test,
                     gconstpointer data)
{
  GAsyncResult *result = NULL;
  CockpitWebService *service;
  JsonObject *response;
  CockpitCreds *creds;
  GError *error = NULL;
  GHashTable *headers;
  JsonObject *login_data;
  const SuccessFixture *fix = data;
  const gchar *path = fix->path ? fix->path : "/cockpit";
  const gchar *application = fix->application ? fix->application : "cockpit";

  if (fix->warning)
    cockpit_expect_warning (fix->warning);

  headers = web_socket_util_new_headers ();
  g_hash_table_insert (headers, g_strdup ("Authorization"), g_strdup (fix->header));
  cockpit_auth_login_async (test->auth,
                            WebRequest(.path=path, .headers=headers),
                            on_ready_get_result, &result);
  g_hash_table_unref (headers);

  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);

  headers = web_socket_util_new_headers ();
  response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
  g_object_unref (result);
  g_assert_no_error (error);
  g_assert (response != NULL);
  json_object_unref (response);

  mock_auth_include_cookie_as_if_client (headers, headers,
                               fix->cookie_name ? fix->cookie_name : "cockpit");
  service = cockpit_auth_check_cookie (test->auth, WebRequest(.path=path, .headers=headers));
  creds = cockpit_web_service_get_creds (service);
  g_assert_cmpstr (application, ==, cockpit_creds_get_application (creds));
  if (fix->expect_stored_password)
    g_assert_cmpstr ("this is the machine password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
  else
    g_assert_null (cockpit_creds_get_password (creds));

  login_data = cockpit_creds_get_login_data (creds);
  if (fix->data)
    g_assert_cmpstr (json_object_get_string_member (login_data, "login"),
                     ==, fix->data);
  else
    g_assert_null (login_data);

  g_hash_table_destroy (headers);
  g_object_unref (service);
}

static const SuccessFixture fixture_ssh_basic = {
  .warning = NULL,
  .data = NULL,
  .header = "Basic bWU6dGhpcyBpcyB0aGUgcGFzc3dvcmQ="
};

static const SuccessFixture fixture_ssh_not_authorized = {
  .warning = NULL,
  .data = NULL,
  .header = "Basic bWU6dGhpcyBpcyB0aGUgcGFzc3dvcmQ=",
};

static const SuccessFixture fixture_ssh_remote_basic = {
  .warning = NULL,
  .data = NULL,
  .header = "Basic cmVtb3RlLXVzZXI6dGhpcyBpcyB0aGUgbWFjaGluZSBwYXNzd29yZA==",
  .path = "/cockpit+=machine",
  .user = "remote-user",
  .application = "cockpit+=machine",
  .cookie_name = "machine-cockpit+machine",
  .expect_stored_password = TRUE
};

static const SuccessFixture fixture_ssh_no_data = {
  .warning = NULL,
  .data = NULL,
  .header = "testsshscheme success"
};

static const SuccessFixture fixture_ssh_remote_switched = {
  .data = NULL,
  .header = "testscheme ssh-remote-switch",
  .path = "/cockpit+=machine",
  .application = "cockpit+=machine",
  .cookie_name = "machine-cockpit+machine"
};

static const SuccessFixture fixture_ssh_alt_default = {
  .data = NULL,
  .header = "testsshscheme ssh-alt-default",
};

static const SuccessFixture fixture_ssh_alt = {
  .data = NULL,
  .path = "/cockpit+=machine",
  .application = "cockpit+=machine",
  .header = "testsshscheme ssh-alt-machine",
  .cookie_name = "machine-cockpit+machine"
};

static const ErrorFixture fixture_bad_conversation = {
  .header = "X-Conversation conversation-id xxx",
  .error_message = "Invalid conversation token",
};

static const ErrorFixture fixture_ssh_basic_failed = {
  .error_message = "Authentication failed",
  .header = "Basic dXNlcjp0aGlzIGlzIHRoZSBwYXNzd29yZA==",
  .init_problem = "authentication-failed",
};

static const ErrorFixture fixture_ssh_remote_basic_failed = {
  .error_message = "Authentication failed",
  .header = "Basic d3Jvbmc6dGhpcyBpcyB0aGUgbWFjaGluZSBwYXNzd29yZA==",
  .path = "/cockpit+=machine",
  .init_problem = "authentication-failed",
};

static const ErrorFixture fixture_ssh_not_supported = {
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication failed: authentication-not-supported",
  .header = "testsshscheme not-supported",
  .init_problem = "authentication-not-supported",
};

static const ErrorFixture fixture_ssh_auth_failed = {
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication failed",
  .header = "testsshscheme ssh-fail",
  .init_problem = "authentication-failed",
};

static const ErrorFixture fixture_ssh_auth_with_error = {
  .error_code = COCKPIT_ERROR_FAILED,
  .error_message = "Authentication failed: unknown: detail for error",
  .header = "testsshscheme with-error",
  .init_problem = "unknown",
};

static const SuccessFixture fixture_no_cookie = {
  .warning = NULL,
  .data = NULL,
  .header = "testscheme no-cookie"
};

static const SuccessFixture fixture_no_data = {
  .warning = NULL,
  .data = NULL,
  .header = "testscheme success"
};

static const SuccessFixture fixture_data_then_success = {
  .warning = NULL,
  .data = "data",
  .header = "testscheme data-then-success"
};

static const ErrorFixture fixture_bad_command = {
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication not available",
  .header = "badcommand bad",
};

static const ErrorFixture fixture_auth_failed = {
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication failed",
  .header = "testscheme fail",
  .init_problem = "authentication-failed",
};

static const ErrorFixture fixture_auth_denied = {
  .error_code = COCKPIT_ERROR_PERMISSION_DENIED,
  .error_message = "Permission denied",
  .header = "testscheme denied",
  .init_problem = "access-denied",
};

static const ErrorFixture fixture_auth_with_error = {
  .error_code = COCKPIT_ERROR_FAILED,
  .error_message = "Authentication failed: unknown: detail for error",
  .header = "testscheme with-error",
  .init_problem = "unknown",
};

static const ErrorFixture fixture_auth_none = {
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication disabled",
  .header = "none invalid",
};

static const ErrorFixture fixture_auth_timeout = {
  .error_message = "Authentication failed: Timeout",
  .header = "timeout-scheme too-slow",
};

static const ErrorFixture fixture_non_ascii = {
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication required",
  .header = "testscheme süccëss",
};

static const ErrorFixture fixture_tls_cert = {
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication required",
  .header = "tls-cert e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
};


typedef struct {
  const gchar **headers;
  const gchar **prompts;
  const gchar *error_message;
  const gchar *message;
  int error_code;
  const gchar *init_problem;
  int pause;
} ErrorMultiFixture;

typedef struct {
  const gchar **headers;
  const gchar **prompts;
} SuccessMultiFixture;

static inline gchar *
str_skip (gchar *v,
          gchar c)
{
  while (v[0] == c)
    v++;
  return v;
}

static gboolean
parse_login_reply_challenge (GHashTable *headers,
                             gchar **out_id,
                             gchar **out_prompt)
{
  gchar *original = NULL;
  gchar *line;
  gchar *next;
  gchar *id = NULL;
  gchar *prompt = NULL;
  gboolean ret = FALSE;
  gpointer key = NULL;
  gsize length;

  if (!g_hash_table_lookup_extended (headers, "WWW-Authenticate", &key, (gpointer *)&original))
    goto out;

  line = original;

  // Check challenge type
  line = str_skip (line, ' ');
  if (g_ascii_strncasecmp (line, "X-Conversation ", strlen("X-Conversation ")) != 0)
    goto out;

  next = strchr (line, ' ');
  if (!next)
    goto out;

  // Get id
  line = next;
  line = str_skip (line, ' ');
  next = strchr (line, ' ');
  if (!next)
    goto out;
  id = g_strndup (line, next - line);

  // Rest should be the base64 prompt
  next = str_skip (next, ' ');
  prompt = g_strdup (next);
  if (g_base64_decode_inplace (prompt, &length) == NULL)
    goto out;
  prompt[length] = '\0';
  ret = TRUE;

out:
  if (ret)
    {
      *out_id = id;
      *out_prompt = prompt;
    }
  else
    {
      g_warning ("Got invalid WWW-Authenticate header: %s", original);
      g_free (id);
      g_free (prompt);
    }

  return ret;
}

static void
test_multi_step_success (Test *test,
                         gconstpointer data)
{
  CockpitWebService *service;
  CockpitCreds *creds;
  GHashTable *headers = NULL;
  gint spot = 0;
  gchar *id = NULL;

  const SuccessMultiFixture *fix = data;

  for (spot = 0; fix->headers[spot]; spot++)
    {
      GAsyncResult *result = NULL;
      JsonObject *response = NULL;
      GError *error = NULL;
      const gchar *header = fix->headers[spot];
      const gchar *expect_prompt = fix->prompts[spot];
      gchar *out = NULL;
      gchar *prompt = NULL;

      headers = web_socket_util_new_headers ();
      if (id)
        {
          g_assert (id != NULL);
          out = g_base64_encode ((guint8 *)header, strlen (header));
          g_hash_table_insert (headers, g_strdup ("Authorization"),
                               g_strdup_printf  ("X-Conversation %s %s", id, out));
          g_free (id);
          g_free (out);
          out = NULL;
          id = NULL;
        }
      else
        {
          g_hash_table_insert (headers, g_strdup ("Authorization"),
                               g_strdup (header));
        }
      cockpit_auth_login_async (test->auth,
                                WebRequest(.path="/cockpit/", .headers=headers),
                                on_ready_get_result, &result);
      g_hash_table_unref (headers);

      while (result == NULL)
        g_main_context_iteration (NULL, TRUE);

      headers = web_socket_util_new_headers ();
      g_assert (headers);
      response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
      g_object_unref (result);

      /* Confirm we got the right prompt */
      if (expect_prompt)
        {
          g_assert (prompt == NULL);
          g_assert (id == NULL);

          g_assert (parse_login_reply_challenge (headers, &id, &prompt));
          g_assert_cmpstr (expect_prompt, ==, prompt);
          g_assert (id != NULL);
          g_free (prompt);
          prompt = NULL;

          g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
          g_clear_error (&error);

          g_hash_table_unref (headers);
        }
      else
        {
          g_assert_no_error (error);
        }

      if (response)
        json_object_unref (response);
    }

  mock_auth_include_cookie_as_if_client (headers, headers, "cockpit");
  service = cockpit_auth_check_cookie (test->auth, WebRequest(.path="/cockpit", .headers=headers));
  creds = cockpit_web_service_get_creds (service);
  g_assert_cmpstr ("cockpit", ==, cockpit_creds_get_application (creds));
  g_assert_null (cockpit_creds_get_password (creds));
  g_hash_table_destroy (headers);
  g_object_unref (service);
  g_free (id);
}

static void
test_multi_step_fail (Test *test,
                      gconstpointer data)
{
  GHashTable *headers = NULL;
  gint spot = 0;
  g_autofree gchar *id = NULL;
  gchar *prompt = NULL;
  const ErrorMultiFixture *fix = data;

  if (fix->message)
    cockpit_expect_message (fix->message);

  for (spot = 0; fix->headers[spot]; spot++)
    {
      GAsyncResult *result = NULL;
      JsonObject *response = NULL;
      GError *error = NULL;
      const gchar *header = fix->headers[spot];
      const gchar *expect_prompt = fix->prompts[spot];
      gchar *out = NULL;
      gboolean ready_for_next = TRUE;

      headers = web_socket_util_new_headers ();
      if (id)
        {
          g_assert (id != NULL);
          out = g_base64_encode ((guint8 *)header, strlen (header));
          g_hash_table_insert (headers, g_strdup ("Authorization"),
                               g_strdup_printf  ("X-Conversation %s %s", id, out));
          g_free (id);
          g_free (out);
          out = NULL;
          id = NULL;
        }
      else
        {
          g_hash_table_insert (headers, g_strdup ("Authorization"),
                               g_strdup (header));
        }
      cockpit_auth_login_async (test->auth,
                                WebRequest(.path="/cockpit/", .headers=headers),
                                on_ready_get_result, &result);
      g_hash_table_unref (headers);

      while (result == NULL)
        g_main_context_iteration (NULL, TRUE);

      headers = web_socket_util_new_headers ();
      response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
      g_object_unref (result);
      g_assert (error != NULL);

      /* Confirm we got the right prompt */
      if (expect_prompt)
        {
          g_assert (prompt == NULL);
          g_assert (id == NULL);

          g_assert (parse_login_reply_challenge (headers, &id, &prompt));
          g_assert_cmpstr (expect_prompt, ==, prompt);
          g_assert (id != NULL);
          g_free (prompt);
          prompt = NULL;
          if (fix->pause)
            {
              ready_for_next = FALSE;
              g_timeout_add_seconds (fix->pause, on_timeout_set_flag, &ready_for_next);
            }

          while (ready_for_next == FALSE)
            g_main_context_iteration (NULL, TRUE);

          if (response)
            json_object_unref (response);
          g_hash_table_unref (headers);

          g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
          g_clear_error (&error);
        }
      else
        {
          if (fix->init_problem)
            {
              g_assert (response != NULL);
              g_assert_cmpstr (json_object_get_string_member (response, "problem"), ==, fix->init_problem);
              g_clear_pointer (&response, json_object_unref);
            }
          else
            {
              g_assert (response == NULL);
            }

          if (fix->error_code)
            g_assert_error (error, COCKPIT_ERROR, fix->error_code);
          else
            g_assert (error != NULL);

          g_assert_cmpstr (fix->error_message, ==, error->message);
          g_clear_error (&error);
          break;
        }
    }

  if (headers)
    g_hash_table_destroy (headers);
}

const gchar *two_steps[3] = { "testscheme two-step", "two", NULL };
const gchar *two_prompts[2] = { "type two", NULL };
const gchar *three_steps[4] = { "testscheme three-step", "two", "three", NULL };
const gchar *three_steps_ssh[4] = { "testsshscheme three-step", "two", "three", NULL };
const gchar *three_prompts[3] = { "type two", "type three", NULL };

static const SuccessMultiFixture fixture_two_steps = {
  .headers = two_steps,
  .prompts = two_prompts,
};

static const SuccessMultiFixture fixture_three_steps = {
  .headers = three_steps,
  .prompts = three_prompts,
};

static const SuccessMultiFixture fixture_ssh_three_steps = {
  .headers = three_steps_ssh,
  .prompts = three_prompts,
};

const gchar *two_steps_ssh_wrong[3] = { "testsshscheme two-step", "bad", NULL };
const gchar *two_steps_wrong[3] = { "testscheme two-step", "bad", NULL };
const gchar *three_steps_wrong[4] = { "testscheme three-step", "two", "bad", NULL };

static const ErrorMultiFixture fixture_fail_three_steps = {
  .headers = three_steps_wrong,
  .prompts = three_prompts,
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication failed",
  .init_problem = "authentication-failed",
};

static const ErrorMultiFixture fixture_fail_two_steps = {
  .headers = two_steps_wrong,
  .prompts = two_prompts,
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication failed",
  .init_problem = "authentication-failed",
};

static const ErrorMultiFixture fixture_fail_ssh_two_steps = {
  .headers = two_steps_ssh_wrong,
  .prompts = two_prompts,
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Authentication failed",
  .init_problem = "authentication-failed",
};

static const ErrorMultiFixture fixture_fail_step_timeout = {
  .headers = two_steps,
  .prompts = two_prompts,
  .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
  .error_message = "Invalid conversation token",
  .message = "*session timed out during authentication*",
  .pause = 3,
};


typedef struct {
  const gchar *str;
  guint max_startups;
  guint max_startups_rate;
  guint max_startups_begin;
  gboolean warn;
} StartupFixture;

static void
setup_startups (Test *test,
                gconstpointer data)
{
  const StartupFixture *fix = data;
  cockpit_config_file = SRCDIR "does-not-exist";
  cockpit_ws_max_startups = fix->str;
  if (fix->warn)
    cockpit_expect_warning ("Illegal MaxStartups spec*");

  test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
}

static void
teardown_startups (Test *test,
                 gconstpointer data)
{
  cockpit_assert_expected ();
  g_object_unref (test->auth);
}

static const StartupFixture fixture_normal = {
  .str = "20:50:200",
  .max_startups = 200,
  .max_startups_begin = 20,
  .max_startups_rate = 50,
  .warn = FALSE,
};

static const StartupFixture fixture_single = {
  .str = "20",
  .max_startups = 20,
  .max_startups_begin = 20,
  .max_startups_rate = 100,
  .warn = FALSE,
};

static const StartupFixture fixture_double = {
  .str = "20:50",
  .max_startups = 20,
  .max_startups_begin = 20,
  .max_startups_rate = 100,
  .warn = FALSE,
};

static const StartupFixture fixture_unlimited = {
  .str = "0",
  .max_startups = 0,
  .max_startups_begin = 0,
  .max_startups_rate = 100,
  .warn = FALSE,
};

static const StartupFixture fixture_bad = {
  .str = "bad",
  .max_startups = 10,
  .max_startups_begin = 10,
  .max_startups_rate = 100,
  .warn = TRUE,
};

static const StartupFixture fixture_bad_rate = {
  .str = "20:101:40",
  .max_startups = 10,
  .max_startups_begin = 10,
  .max_startups_rate = 100,
  .warn = TRUE,
};

static const StartupFixture fixture_bad_startups = {
  .str = "40:101:20",
  .max_startups = 10,
  .max_startups_begin = 10,
  .max_startups_rate = 100,
  .warn = TRUE,
};

static const StartupFixture fixture_bad_negative = {
  .str = "-40:101:20",
  .max_startups = 10,
  .max_startups_begin = 10,
  .max_startups_rate = 100,
  .warn = TRUE,
};

static const StartupFixture fixture_bad_too_many = {
  .str = "40:101:20:50:50",
  .max_startups = 10,
  .max_startups_begin = 10,
  .max_startups_rate = 100,
  .warn = TRUE,
};

static void
test_max_startups_conf (Test *test,
                        gconstpointer data)
{
  const StartupFixture *fix = data;
  g_assert_cmpuint (fix->max_startups_begin, ==, test->auth->max_startups_begin);
  g_assert_cmpuint (fix->max_startups,  ==, test->auth->max_startups);
  g_assert_cmpuint (fix->max_startups_rate,  ==, test->auth->max_startups_rate);
}

int
main (int argc,
      char *argv[])
{
  cockpit_ws_session_program = BUILDDIR "/mock-auth-command";
  cockpit_ws_service_idle = 1;

  cockpit_test_setenv ("COCKPIT_WS_PROCESS_IDLE", "2");

  cockpit_test_init (&argc, &argv);

  g_test_add ("/auth/application", Test, NULL, NULL, test_application, NULL);
  g_test_add ("/auth/userpass-header-check", Test, NULL, setup, test_userpass_cookie_check, teardown);
  g_test_add ("/auth/userpass-store-check", Test, &fixture_superuser_any, setup, test_userpass_cookie_check, teardown);
  g_test_add ("/auth/userpass-bad", Test, NULL, setup, test_userpass_bad, teardown);
  g_test_add ("/auth/userpass-emptypass", Test, NULL, setup, test_userpass_emptypass, teardown);
  g_test_add ("/auth/headers-bad", Test, NULL, setup, test_headers_bad, teardown);
  g_test_add ("/auth/idle-timeout", Test, NULL, setup, test_idle_timeout, teardown);
  g_test_add ("/auth/process-timeout", Test, NULL, setup, test_process_timeout, teardown);
  g_test_add ("/auth/bad-coversation", Test, &fixture_bad_conversation,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/custom-success", Test, &fixture_no_data,
              setup_normal, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-no-cookie-success", Test, &fixture_no_cookie,
              setup_normal, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-data-then-success", Test, &fixture_data_then_success,
              setup_normal, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-fail-auth", Test, &fixture_auth_failed,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/custom-denied-auth", Test, &fixture_auth_denied,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/custom-with-error", Test, &fixture_auth_with_error,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/custom-timeout", Test, &fixture_auth_timeout,
              setup_normal, test_custom_timeout, teardown_normal);
  g_test_add ("/auth/non-ascii", Test, &fixture_non_ascii,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/bogus-tls-cert", Test, &fixture_tls_cert,
              setup_normal, test_custom_fail, teardown_normal);

  g_test_add ("/auth/custom-ssh-basic-success", Test, &fixture_ssh_basic,
              setup_normal, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-ssh-basic-success-not-authorized", Test, &fixture_ssh_not_authorized,
              setup_normal, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-ssh-remote-basic-success", Test, &fixture_ssh_remote_basic,
              setup_normal, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-ssh-remote-switched", Test, &fixture_ssh_remote_switched,
              setup_normal, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-ssh-with-conf-default", Test, &fixture_ssh_alt_default,
              setup_alt_config, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-ssh-with-conf-allow", Test, &fixture_ssh_alt,
              setup_alt_config, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-ssh-success", Test, &fixture_ssh_no_data,
              setup_normal, test_custom_success, teardown_normal);
  g_test_add ("/auth/custom-ssh-fail-auth", Test, &fixture_ssh_auth_failed,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/custom-ssh-fail-basic-auth", Test, &fixture_ssh_basic_failed,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/custom-ssh-remote-fail-basic-auth", Test, &fixture_ssh_remote_basic_failed,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/custom-ssh-not-supported", Test, &fixture_ssh_not_supported,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/custom-ssh-with-error", Test, &fixture_ssh_auth_with_error,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/success-ssh-multi-step-three", Test, &fixture_ssh_three_steps,
              setup_normal, test_multi_step_success, teardown_normal);
  g_test_add ("/auth/fail-ssh-multi-step-two", Test, &fixture_fail_ssh_two_steps,
              setup_normal, test_multi_step_fail, teardown_normal);

  g_test_add ("/auth/none", Test, &fixture_auth_none,
              setup_normal, test_custom_fail, teardown_normal);
  g_test_add ("/auth/bad-command", Test, &fixture_bad_command,
              setup_normal, test_bad_command, teardown_normal);
  g_test_add ("/auth/success-multi-step-two", Test, &fixture_two_steps,
              setup_normal, test_multi_step_success, teardown_normal);
  g_test_add ("/auth/success-multi-step-three", Test, &fixture_three_steps,
              setup_normal, test_multi_step_success, teardown_normal);
  g_test_add ("/auth/fail-multi-step-two", Test, &fixture_fail_two_steps,
              setup_normal, test_multi_step_fail, teardown_normal);
  g_test_add ("/auth/fail-multi-step-three", Test, &fixture_fail_three_steps,
              setup_normal, test_multi_step_fail, teardown_normal);
  g_test_add ("/auth/fail-multi-step-timeout", Test, &fixture_fail_step_timeout,
              setup_normal, test_multi_step_fail, teardown_normal);
  g_test_add ("/auth/max-startups", Test, NULL,
              setup_normal, test_max_startups, teardown_normal);
  g_test_add ("/auth/max-startups-normal", Test, &fixture_normal,
              setup_startups, test_max_startups_conf, teardown_startups);
  g_test_add ("/auth/max-startups-single", Test, &fixture_single,
              setup_startups, test_max_startups_conf, teardown_startups);
  g_test_add ("/auth/max-startups-double", Test, &fixture_double,
              setup_startups, test_max_startups_conf, teardown_startups);
  g_test_add ("/auth/max-startups-unlimited", Test, &fixture_unlimited,
              setup_startups, test_max_startups_conf, teardown_startups);
  g_test_add ("/auth/max-startups-bad", Test, &fixture_bad,
              setup_startups, test_max_startups_conf, teardown_startups);
  g_test_add ("/auth/max-startups-bad-rate", Test, &fixture_bad_rate,
              setup_startups, test_max_startups_conf, teardown_startups);
  g_test_add ("/auth/max-startups-bad-startups", Test, &fixture_bad_startups,
              setup_startups, test_max_startups_conf, teardown_startups);
  g_test_add ("/auth/max-startups-bad-negative", Test, &fixture_bad_negative,
              setup_startups, test_max_startups_conf, teardown_startups);
  g_test_add ("/auth/max-startups-too-many", Test, &fixture_bad_too_many,
              setup_startups, test_max_startups_conf, teardown_startups);
  return g_test_run ();
}
