#include "lib.h"
#include "str.h"
#include "strfuncs.h"
#include "test-common.h"
#include "dsasl-client.h"

static const struct dsasl_client_settings sasl_empty_set = {
	.authid = NULL,
};

static const struct dsasl_client_settings sasl_no_password_set = {
	.authid = "testuser",
};

static const struct dsasl_client_settings sasl_set = {
	.authid = "testuser",
	.password = "testpassword"
};

static const struct dsasl_client_settings sasl_master_set = {
	.authzid = "testuser",
	.authid = "masteruser",
	.password = "masterpassword"
};

static void test_sasl_client_login(void)
{
	const char *error;
	test_begin("sasl client LOGIN");
	const struct dsasl_client_mech *mech = dsasl_client_mech_find("login");
	i_assert(mech != NULL);
	struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	string_t *input = t_str_new(64);
	string_t *output_s = t_str_new(64);
	const unsigned char *output;
	size_t olen;

	/* parameters shouldn't work (test here for completeness) */
	test_assert(dsasl_client_set_parameter(client, "parameter", "value", &error) == 0);

	/* Any input is valid */
	str_append(input, "Username:");
	test_assert(dsasl_client_input(client, input->data, input->used, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	/* see what we got */
	str_append_data(output_s, output, olen);
	test_assert_strcmp(str_c(output_s), "testuser");

	str_truncate(input, 0);
	str_truncate(output_s, 0);

	str_append(input, "Password:");
	test_assert(dsasl_client_input(client, input->data, input->used, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	str_append_data(output_s, output, olen);
	test_assert_strcmp(str_c(output_s), "testpassword");

	/* there should be no results to collect, we can reuse error here */
	test_assert(dsasl_client_get_result(client, "parameter", &error, &error) == 0);

	dsasl_client_free(&client);
	test_assert(client == NULL);

	/* server sends input after password */
	client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_ERR_PROTOCOL);
	test_assert_strcmp(error, "Server didn't finish authentication");

	dsasl_client_free(&client);

	/* missing username & password */
	client = dsasl_client_new(mech, &sasl_empty_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_ERR_INTERNAL);
	test_assert_strcmp(error, "authid not set");
	dsasl_client_free(&client);

	/* missing password */
	client = dsasl_client_new(mech, &sasl_no_password_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_ERR_INTERNAL);
	test_assert_strcmp(error, "password not set");
	dsasl_client_free(&client);

	/* unexpected NUL byte in input */
	client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	str_truncate(input, 0);
	str_append_data(input, "unexpected\0", 11);
	test_assert(dsasl_client_input(client, input->data, input->used, &error) == DSASL_CLIENT_RESULT_ERR_PROTOCOL);
	test_assert_strcmp(error, "Unexpected NUL in input data");
	dsasl_client_free(&client);

	test_end();
}

static void test_sasl_client_plain(void)
{
	const char *error;
	test_begin("sasl client PLAIN");
	const struct dsasl_client_mech *mech = dsasl_client_mech_find("plain");
	i_assert(mech != NULL);
	struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	const unsigned char *output;
	size_t olen;

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	const unsigned char expected[] = "\0testuser\0testpassword";
	/* there is no NUL byte at the end */
	test_assert(olen == sizeof(expected) - 1);
	test_assert(memcmp(output, expected, I_MIN(sizeof(expected) - 1, olen)) == 0);

	dsasl_client_free(&client);
	test_assert(client == NULL);

	client = dsasl_client_new(mech, &sasl_master_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	const unsigned char expected_master[] =
		"testuser\0masteruser\0masterpassword";
	/* there is no NUL byte at the end */
	test_assert(olen == sizeof(expected_master) - 1);
	test_assert(memcmp(output, expected_master,
			   I_MIN(sizeof(expected_master) - 1, olen)) == 0);

	dsasl_client_free(&client);

	/* unexpected initial response */
	const unsigned char input[] = "ir";
	client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);
	test_assert(dsasl_client_input(client, input, sizeof(input)-1, &error) == DSASL_CLIENT_RESULT_ERR_PROTOCOL);
	test_assert_strcmp(error, "Server sent non-empty initial response");

	dsasl_client_free(&client);

	/* server sends input after response */
	client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_ERR_PROTOCOL);
	test_assert_strcmp(error, "Server didn't finish authentication");

	dsasl_client_free(&client);

	/* missing username & password */
	client = dsasl_client_new(mech, &sasl_empty_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_ERR_INTERNAL);
	test_assert_strcmp(error, "authid not set");
	dsasl_client_free(&client);

	/* missing password */
	client = dsasl_client_new(mech, &sasl_no_password_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_ERR_INTERNAL);
	test_assert_strcmp(error, "password not set");
	dsasl_client_free(&client);

	/* unexpected NUL byte in input */
	client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	const unsigned char input2[] = "unexpected\0";
	test_assert(dsasl_client_input(client, input2, sizeof(input2), &error) == DSASL_CLIENT_RESULT_ERR_PROTOCOL);
	test_assert_strcmp(error, "Unexpected NUL in input data");
	dsasl_client_free(&client);

	test_end();
}

static void test_sasl_client_external(void)
{
	const char *error;
	test_begin("sasl client EXTERNAL");
	const struct dsasl_client_mech *mech = dsasl_client_mech_find("external");
	i_assert(mech != NULL);
	struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	const unsigned char *output;
	size_t olen;

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	const unsigned char expected[] = "testuser";
	/* there is no NUL byte at the end */
	test_assert(olen == sizeof(expected) - 1);
	test_assert(memcmp(output, expected, I_MIN(sizeof(expected) - 1, olen)) == 0);

	dsasl_client_free(&client);
	test_assert(client == NULL);

	client = dsasl_client_new(mech, &sasl_master_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	const unsigned char expected_master[] =	"testuser";
	/* there is no NUL byte at the end */
	test_assert(olen == sizeof(expected_master) - 1);
	test_assert(memcmp(output, expected_master,
			   I_MIN(sizeof(expected_master) - 1, olen)) == 0);

	dsasl_client_free(&client);

	/* unexpected NUL byte in input */
	client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	const unsigned char input2[] = "unexpected\0";
	test_assert(dsasl_client_input(client, input2, sizeof(input2), &error) == DSASL_CLIENT_RESULT_ERR_PROTOCOL);
	test_assert_strcmp(error, "Unexpected NUL in input data");
	dsasl_client_free(&client);

	test_end();
}

static void test_sasl_client_oauthbearer(void)
{
	const char *error;
	const char *value;
	test_begin("sasl client OAUTHBEARER");
	const struct dsasl_client_mech *mech = dsasl_client_mech_find("oauthbearer");
	i_assert(mech != NULL);
	struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	string_t *input = t_str_new(64);
	const unsigned char *output;
	size_t olen;

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);

	const unsigned char expected[] = "n,a=testuser,\1"
		"auth=Bearer testpassword\1\1";
	/* there is no NUL byte at the end */
	test_assert(olen == sizeof(expected) - 1);
	test_assert(memcmp(output, expected, I_MIN(sizeof(expected) - 1, olen)) == 0);
	test_assert(dsasl_client_get_result(client, "status", &value, &error) == 1);
	test_assert_strcmp(value, "");

	dsasl_client_free(&client);
	test_assert(client == NULL);

	/* with host & port set */
	struct dsasl_client_settings sasl_set_host = sasl_set;
	sasl_set_host.host = "example.com";
	sasl_set_host.port = 143;
	client = dsasl_client_new(mech, &sasl_set_host);
	i_assert(client != NULL);

	test_assert(dsasl_client_set_parameter(client, "unknown", "value", &error) == 0);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);

	const unsigned char expected_h_p[] = "n,a=testuser,\1"
		"host=example.com\1"
		"port=143\1"
		"auth=Bearer testpassword\1\1";
	/* there is no NUL byte at the end */
	test_assert(olen == sizeof(expected_h_p) - 1);
	test_assert(memcmp(output, expected_h_p,
			   I_MIN(sizeof(expected_h_p) - 1, olen)) == 0);

	dsasl_client_free(&client);
	test_assert(client == NULL);

	client = dsasl_client_new(mech, &sasl_set);
	/* test error response */
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	str_append(input, "{\"status\":\"401\",\"schemes\":\"bearer\",\"scope\":\"mail\"}");
	test_assert(dsasl_client_input(client, input->data, input->used, &error) == DSASL_CLIENT_RESULT_AUTH_FAILED);
	test_assert_strcmp(error, "Failed to authenticate: 401");
	test_assert(dsasl_client_get_result(client, "status", &value, &error) == 1);
	test_assert_strcmp(value, "401");

	dsasl_client_free(&client);

	/* missing username & password */
	client = dsasl_client_new(mech, &sasl_empty_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_ERR_INTERNAL);
	test_assert_strcmp(error, "authid not set");
	dsasl_client_free(&client);

	/* missing password */
	client = dsasl_client_new(mech, &sasl_no_password_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_ERR_INTERNAL);
	test_assert_strcmp(error, "password not set");
	dsasl_client_free(&client);

	/* unexpected NUL byte in input */
	client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	const unsigned char input2[] = "unexpected\0";
	test_assert(dsasl_client_input(client, input2, sizeof(input2), &error) == DSASL_CLIENT_RESULT_ERR_PROTOCOL);
	test_assert_strcmp(error, "Unexpected NUL in input data");
	dsasl_client_free(&client);

	test_end();
}

static void test_sasl_client_xoauth2(void)
{
	const char *error;
	test_begin("sasl client XOAUTH2");
	const struct dsasl_client_mech *mech = dsasl_client_mech_find("xoauth2");
	i_assert(mech != NULL);
	struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	string_t *input = t_str_new(64);
	const unsigned char *output;
	size_t olen;

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);

	const unsigned char expected[] = "user=testuser\1auth=Bearer testpassword\1\1";
	/* there is no NUL byte at the end */
	test_assert(olen == sizeof(expected) - 1);
	test_assert(memcmp(output, expected, I_MIN(sizeof(expected) - 1, olen)) == 0);

	dsasl_client_free(&client);
	test_assert(client == NULL);

	client = dsasl_client_new(mech, &sasl_set);
	/* test error response */
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	str_append(input, "{\"status\":\"401\",\"schemes\":\"bearer\",\"scope\":\"mail\"}");
	test_assert(dsasl_client_input(client, input->data, input->used, &error) == DSASL_CLIENT_RESULT_AUTH_FAILED);
	test_assert_strcmp(error, "Failed to authenticate: 401");

	dsasl_client_free(&client);

	/* missing username & password */
	client = dsasl_client_new(mech, &sasl_empty_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_ERR_INTERNAL);
	test_assert_strcmp(error, "authid not set");
	dsasl_client_free(&client);

	/* missing password */
	client = dsasl_client_new(mech, &sasl_no_password_set);
	i_assert(client != NULL);

	test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == DSASL_CLIENT_RESULT_OK);
	test_assert(dsasl_client_output(client, &output, &olen, &error) == DSASL_CLIENT_RESULT_ERR_INTERNAL);
	test_assert_strcmp(error, "password not set");
	dsasl_client_free(&client);

	/* unexpected NUL byte in input */
	client = dsasl_client_new(mech, &sasl_set);
	i_assert(client != NULL);

	const unsigned char input2[] = "unexpected\0";
	test_assert(dsasl_client_input(client, input2, sizeof(input2), &error) == DSASL_CLIENT_RESULT_ERR_PROTOCOL);
	test_assert_strcmp(error, "Unexpected NUL in input data");
	dsasl_client_free(&client);

	test_end();
}

int main(void)
{
	static void (*const test_functions[])(void) = {
		test_sasl_client_login,
		test_sasl_client_plain,
		test_sasl_client_external,
		test_sasl_client_oauthbearer,
		test_sasl_client_xoauth2,
		NULL
	};
	dsasl_clients_init();
	int ret = test_run(test_functions);
	dsasl_clients_deinit();
	return ret;
}
