/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
   Copyright (C) 2010 Collabora Ltd

   The Gnome Keyring Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public License as
   published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.

   The Gnome Keyring Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with the Gnome Library; see the file COPYING.LIB.  If not,
   see <http://www.gnu.org/licenses/>.

   Author: Stef Walter <stefw@collabora.co.uk>
*/

#include "config.h"

#include "gcr/gcr-base.h"
#include "gcr/gcr-subject-public-key.h"

#include "gck/gck-mock.h"
#include "gck/gck-test.h"

#include "egg/egg-asn1x.h"
#include "egg/egg-asn1-defs.h"
#include "egg/egg-testing.h"

#include <glib.h>

#include <errno.h>

typedef struct {
	const gchar *name;
	const gchar *basename;
	guint key_size;
} TestFixture;

typedef struct {
	GBytes *crt_data;
	GckAttributes *crt_attrs;
	GBytes *key_data;
	GckAttributes *prv_attrs;
	GBytes *spk_data;
	GckAttributes *pub_attrs;
} TestAttributes;

static void
on_parser_parsed (GcrParser *parser,
                  gpointer user_data)
{
	GckAttributes **attrs = user_data;
	g_assert (*attrs == NULL);
	*attrs = gcr_parser_get_parsed_attributes (parser);
	g_assert (*attrs != NULL);
	gck_attributes_ref (*attrs);
}

static GckAttributes *
parse_attributes (GBytes *data)
{
	GcrParser *parser;
	GckAttributes *attrs = NULL;
	GError *error = NULL;

	parser = gcr_parser_new ();
	g_signal_connect (parser, "parsed", G_CALLBACK (on_parser_parsed), &attrs);
	gcr_parser_parse_bytes (parser, data, &error);
	g_assert_no_error (error);
	g_object_unref (parser);

	g_assert (attrs);
	return attrs;
}

static void
setup_attributes (TestAttributes *test,
                  gconstpointer data)
{
	const TestFixture *fixture = data;
	GError *error = NULL;
	gchar *contents;
	gchar *filename;
	gsize length;
	gulong klass;

	filename = g_strdup_printf (SRCDIR "/gcr/fixtures/%s.crt", fixture->basename);
	g_file_get_contents (filename, &contents, &length, &error);
	g_assert_no_error (error);
	test->crt_data = g_bytes_new_take (contents, length);
	test->crt_attrs = parse_attributes (test->crt_data);
	g_assert (gck_attributes_find_ulong (test->crt_attrs, CKA_CLASS, &klass));
	gck_assert_cmpulong (klass, ==, CKO_CERTIFICATE);
	g_free (filename);

	filename = g_strdup_printf (SRCDIR "/gcr/fixtures/%s.key", fixture->basename);
	g_file_get_contents (filename, &contents, &length, &error);
	g_assert_no_error (error);
	test->key_data = g_bytes_new_take (contents, length);
	test->prv_attrs = parse_attributes (test->key_data);
	g_assert (gck_attributes_find_ulong (test->prv_attrs, CKA_CLASS, &klass));
	gck_assert_cmpulong (klass, ==, CKO_PRIVATE_KEY);
	g_free (filename);

	filename = g_strdup_printf (SRCDIR "/gcr/fixtures/%s.spk", fixture->basename);
	g_file_get_contents (filename, &contents, &length, &error);
	g_assert_no_error (error);
	test->spk_data = g_bytes_new_take (contents, length);
	test->pub_attrs = parse_attributes (test->spk_data);
	g_assert (gck_attributes_find_ulong (test->pub_attrs, CKA_CLASS, &klass));
	gck_assert_cmpulong (klass, ==, CKO_PUBLIC_KEY);
	g_free (filename);
}

static void
teardown_attributes (TestAttributes *test,
                     gconstpointer unused)
{
	g_bytes_unref (test->crt_data);
	g_bytes_unref (test->key_data);
	g_bytes_unref (test->spk_data);
	gck_attributes_unref (test->crt_attrs);
	gck_attributes_unref (test->prv_attrs);
	gck_attributes_unref (test->pub_attrs);
}

static void
perform_for_attributes (TestAttributes *test,
                        GckAttributes *attrs)
{
	GNode *info;
	GBytes *data;

	info = _gcr_subject_public_key_for_attributes (attrs);
	g_assert (info != NULL);

	data = egg_asn1x_encode (info, NULL);
	egg_assert_cmpbytes (data, ==, g_bytes_get_data (test->spk_data, NULL),
	                               g_bytes_get_size (test->spk_data));

	g_bytes_unref (data);
	egg_asn1x_destroy (info);

}

static void
test_for_cert_attributes (TestAttributes *test,
                          gconstpointer unused)
{
	perform_for_attributes (test, test->crt_attrs);
}

static void
test_for_private_key_attributes (TestAttributes *test,
                                 gconstpointer unused)
{
	perform_for_attributes (test, test->prv_attrs);
}

static void
test_for_public_key_attributes (TestAttributes *test,
                                gconstpointer unused)
{
	perform_for_attributes (test, test->pub_attrs);
}

static void
perform_calculate_size (TestAttributes *test,
                        GckAttributes *attrs,
                        const TestFixture *fixture)
{
	GNode *info;
	guint size;

	info = _gcr_subject_public_key_for_attributes (attrs);
	g_assert (info != NULL);

	/* TODO: until encoding, we don't have readable attributes */
	g_bytes_unref (egg_asn1x_encode (info, NULL));

	size = _gcr_subject_public_key_calculate_size (info);
	g_assert_cmpuint (size, ==, fixture->key_size);

	egg_asn1x_destroy (info);

}

static void
test_certificate_calculate_size (TestAttributes *test,
                                 gconstpointer fixture)
{
	perform_calculate_size (test, test->crt_attrs, fixture);
}

static void
test_public_key_calculate_size (TestAttributes *test,
                                gconstpointer fixture)
{
	perform_calculate_size (test, test->pub_attrs, fixture);
}

static void
test_private_key_calculate_size (TestAttributes *test,
                                 gconstpointer fixture)
{
	perform_calculate_size (test, test->prv_attrs, fixture);
}

typedef struct {
	CK_FUNCTION_LIST funcs;
	GckModule *module;
	GckSession *session;
} TestModule;

static void
setup_module (TestModule *test,
               gconstpointer unused)
{
	CK_FUNCTION_LIST_PTR f;
	GError *error = NULL;
	GckSlot *slot;
	CK_RV rv;

	rv = gck_mock_C_GetFunctionList (&f);
	gck_assert_cmprv (rv, ==, CKR_OK);
	memcpy (&test->funcs, f, sizeof (test->funcs));

	/* Open a session */
	rv = (test->funcs.C_Initialize) (NULL);
	gck_assert_cmprv (rv, ==, CKR_OK);

	test->module = gck_module_new (&test->funcs);
	g_object_add_weak_pointer (G_OBJECT (test->module), (gpointer *)&test->module);

	slot = gck_slot_from_handle (test->module, GCK_MOCK_SLOT_ONE_ID);
	test->session = gck_session_open (slot, GCK_SESSION_READ_ONLY, NULL, NULL, &error);
	g_assert_no_error (error);
	g_object_add_weak_pointer (G_OBJECT (test->session), (gpointer *)&test->session);

	g_object_unref (slot);
}

static void
teardown_module (TestModule *test,
                 gconstpointer fixture)
{
	CK_RV rv;

	g_object_unref (test->session);
	g_assert (test->session == NULL);

	g_object_unref (test->module);
	g_assert (test->module == NULL);

	rv = (test->funcs.C_Finalize) (NULL);
	gck_assert_cmprv (rv, ==, CKR_OK);
}

typedef struct {
	TestAttributes at;
	TestModule mo;
	GckObject *crt_object;
	GckObject *pub_object;
	GckObject *prv_object;
} TestLoading;

static void
setup_loading (TestLoading *test,
               gconstpointer fixture)
{
	GckBuilder builder = GCK_BUILDER_INIT;
	const gchar *id = "test-id";
	gulong handle;

	setup_attributes (&test->at, fixture);
	setup_module (&test->mo, NULL);

	gck_builder_add_all (&builder, test->at.crt_attrs);
	gck_builder_add_string (&builder, CKA_ID, id);
	handle = gck_mock_module_add_object (gck_builder_end (&builder));
	test->crt_object = gck_object_from_handle (test->mo.session, handle);
	g_object_add_weak_pointer (G_OBJECT (test->crt_object), (gpointer *)&test->crt_object);

	gck_builder_add_all (&builder, test->at.pub_attrs);
	gck_builder_add_string (&builder, CKA_ID, id);
	handle = gck_mock_module_add_object (gck_builder_end (&builder));
	test->pub_object = gck_object_from_handle (test->mo.session, handle);
	g_object_add_weak_pointer (G_OBJECT (test->pub_object), (gpointer *)&test->pub_object);

	gck_builder_add_all (&builder, test->at.prv_attrs);
	gck_builder_add_string (&builder, CKA_ID, id);
	handle = gck_mock_module_add_object (gck_builder_end (&builder));
	test->prv_object = gck_object_from_handle (test->mo.session, handle);
	g_object_add_weak_pointer (G_OBJECT (test->prv_object), (gpointer *)&test->prv_object);
}

static void
teardown_loading (TestLoading *test,
                  gconstpointer fixture)
{
	g_object_unref (test->crt_object);
	g_assert (test->crt_object == NULL);

	g_object_unref (test->prv_object);
	g_assert (test->prv_object == NULL);

	g_object_unref (test->pub_object);
	g_assert (test->pub_object == NULL);

	teardown_module (&test->mo, NULL);
	teardown_attributes (&test->at, fixture);
}

static void
perform_load (TestLoading *test,
              GckObject *object)
{
	GError *error = NULL;
	GBytes *data;
	GNode *info;

	info = _gcr_subject_public_key_load (object, NULL, &error);
	g_assert_no_error (error);
	g_assert (info != NULL);

	data = egg_asn1x_encode (info, NULL);
	egg_assert_cmpbytes (data, ==, g_bytes_get_data (test->at.spk_data, NULL),
	                               g_bytes_get_size (test->at.spk_data));

	g_bytes_unref (data);
	egg_asn1x_destroy (info);
}

static void
test_certificate_load (TestLoading *test,
                       gconstpointer unused)
{
	perform_load (test, test->crt_object);
}

static void
test_public_key_load (TestLoading *test,
                      gconstpointer unused)
{
	perform_load (test, test->pub_object);
}

static void
test_private_key_load (TestLoading *test,
                       gconstpointer unused)
{
	perform_load (test, test->prv_object);
}

static void
on_async_result (GObject *source,
                 GAsyncResult *result,
                 gpointer user_data)
{
	GAsyncResult **ret = user_data;
	g_assert (ret != NULL);
	g_assert (*ret == NULL);
	g_assert (G_IS_ASYNC_RESULT (result));
	*ret = g_object_ref (result);
	egg_test_wait_stop ();
}

static void
perform_load_async (TestLoading *test,
                    GckObject *object)
{
	GAsyncResult *result = NULL;
	GError *error = NULL;
	GBytes *data;
	GNode *info;

	_gcr_subject_public_key_load_async (object, NULL, on_async_result, &result);
	g_assert (result == NULL);
	egg_test_wait ();

	g_assert (result != NULL);
	info = _gcr_subject_public_key_load_finish (result, &error);
	g_assert_no_error (error);
	g_assert (info != NULL);
	g_object_unref (result);

	data = egg_asn1x_encode (info, NULL);
	egg_assert_cmpbytes (data, ==, g_bytes_get_data (test->at.spk_data, NULL),
	                               g_bytes_get_size (test->at.spk_data));

	g_bytes_unref (data);
	egg_asn1x_destroy (info);
}

static void
test_certificate_load_async (TestLoading *test,
                             gconstpointer unused)
{
	perform_load_async (test, test->crt_object);
}

static void
test_public_key_load_async (TestLoading *test,
                            gconstpointer unused)
{
	perform_load_async (test, test->pub_object);
}

static void
test_private_key_load_async (TestLoading *test,
                             gconstpointer unused)
{
	perform_load_async (test, test->prv_object);
}

enum { PROP_ATTRIBUTES = 1 };
typedef struct { GckObject parent; GckAttributes *attrs; } MockObject;
typedef struct { GckObjectClass parent; } MockObjectClass;

GType mock_object_get_type (void) G_GNUC_CONST;
static void mock_object_cache_init (GckObjectCacheIface *iface);
G_DEFINE_TYPE_WITH_CODE (MockObject, mock_object, GCK_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (GCK_TYPE_OBJECT_CACHE, mock_object_cache_init)
);

static void
mock_object_init (MockObject *self)
{

}

static void
mock_object_get_property (GObject *obj,
                          guint prop_id,
                          GValue *value,
                          GParamSpec *pspec)
{
	g_assert (prop_id == PROP_ATTRIBUTES);
	g_value_set_boxed (value, ((MockObject *)obj)->attrs);
}

static void
mock_object_set_property (GObject *obj,
                          guint prop_id,
                          const GValue *value,
                          GParamSpec *pspec)
{
	g_assert (prop_id == PROP_ATTRIBUTES);
	((MockObject *)obj)->attrs = g_value_dup_boxed (value);
}

static void
mock_object_finalize (GObject *obj)
{
	MockObject *self = (MockObject *)obj;
	gck_attributes_unref (self->attrs);
	G_OBJECT_CLASS (mock_object_parent_class)->finalize (obj);
}

static void
mock_object_class_init (MockObjectClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

	gobject_class->get_property = mock_object_get_property;
	gobject_class->set_property = mock_object_set_property;
	gobject_class->finalize = mock_object_finalize;

	g_object_class_override_property (gobject_class, PROP_ATTRIBUTES, "attributes");
}

static void
mock_object_fill (GckObjectCache *object,
                  GckAttributes *attrs)
{
	GckBuilder builder = GCK_BUILDER_INIT;
	MockObject *self = (MockObject *)object;

	gck_builder_add_all (&builder, self->attrs);
	gck_builder_set_all (&builder, attrs);

	gck_attributes_unref (self->attrs);
	self->attrs = gck_attributes_ref_sink (gck_builder_end (&builder));
}

static void
mock_object_cache_init (GckObjectCacheIface *iface)
{
	iface->default_types = NULL;
	iface->n_default_types = 0;
	iface->fill = mock_object_fill;
}

static void
perform_load_already (TestLoading *test,
                      GckAttributes *attributes)
{
	const gulong INVALID = 0xFFF00FF; /* invalid handle, should not be used */
	GckObject *object;
	GError *error = NULL;
	GBytes *data;
	GNode *info;

	object = g_object_new (mock_object_get_type (),
	                       "module", test->mo.module,
	                       "session", test->mo.session,
	                       "handle", INVALID,
	                       "attributes", attributes,
	                       NULL);

	info = _gcr_subject_public_key_load (object, NULL, &error);
	g_assert_no_error (error);
	g_assert (info != NULL);

	data = egg_asn1x_encode (info, NULL);
	egg_assert_cmpbytes (data, ==, g_bytes_get_data (test->at.spk_data, NULL),
	                               g_bytes_get_size (test->at.spk_data));

	g_bytes_unref (data);
	egg_asn1x_destroy (info);
	g_object_unref (object);
}

static void
test_certificate_load_already (TestLoading *test,
                               gconstpointer unused)
{
	perform_load_already (test, test->at.crt_attrs);
}

static void
test_public_key_load_already (TestLoading *test,
                              gconstpointer unused)
{
	perform_load_already (test, test->at.pub_attrs);
}

static void
test_private_key_load_already (TestLoading *test,
                               gconstpointer unused)
{
	perform_load_already (test, test->at.prv_attrs);
}

static void
perform_load_partial (TestLoading *test,
                      GckObject *original,
                      GckAttributes *attributes)
{
	GckBuilder builder = GCK_BUILDER_INIT;
	GckAttributes *partial;
	GckObject *object;
	GError *error = NULL;
	GBytes *data;
	GNode *info;
	guint i;

	for (i = 0; i < gck_attributes_count (attributes); i += 2)
		gck_builder_add_attribute (&builder, gck_attributes_at (attributes, i));
	partial = gck_attributes_ref_sink (gck_builder_end (&builder));

	object = g_object_new (mock_object_get_type (),
	                       "module", test->mo.module,
	                       "session", test->mo.session,
	                       "handle", gck_object_get_handle (original),
	                       "attributes", partial,
	                       NULL);
	gck_attributes_unref (partial);

	info = _gcr_subject_public_key_load (object, NULL, &error);
	g_assert_no_error (error);
	g_assert (info != NULL);

	data = egg_asn1x_encode (info, NULL);
	egg_assert_cmpbytes (data, ==, g_bytes_get_data (test->at.spk_data, NULL),
	                               g_bytes_get_size (test->at.spk_data));

	g_bytes_unref (data);
	egg_asn1x_destroy (info);
	g_object_unref (object);
}

static void
test_certificate_load_partial (TestLoading *test,
                               gconstpointer unused)
{
	perform_load_partial (test, test->crt_object, test->at.crt_attrs);
}

static void
test_public_key_load_partial (TestLoading *test,
                              gconstpointer unused)
{
	perform_load_partial (test, test->pub_object, test->at.pub_attrs);
}

static void
test_private_key_load_partial (TestLoading *test,
                               gconstpointer unused)
{
	perform_load_partial (test, test->prv_object, test->at.prv_attrs);
}

static void
test_load_failure_lookup (TestModule *test,
                          gconstpointer fixture)
{
	const gulong INVALID = 0xFFF00FF; /* invalid handle, should fail */
	GckObject *object;
	GError *error = NULL;
	GNode *info;

	object = g_object_new (mock_object_get_type (),
	                       "module", test->module,
	                       "session", test->session,
	                       "handle", INVALID,
	                       NULL);

	info = _gcr_subject_public_key_load (object, NULL, &error);
	g_assert_error (error, GCK_ERROR, CKR_OBJECT_HANDLE_INVALID);
	g_assert (info == NULL);
	g_error_free (error);

	g_object_unref (object);
}

static void
test_load_failure_build (TestModule *test,
                         gconstpointer fixture)
{
	GckBuilder builder = GCK_BUILDER_INIT;
	GckAttributes *attributes;
	const gulong INVALID = 0xFFF00FF; /* invalid handle, shouldn't be used */
	GckObject *object;
	GError *error = NULL;
	GNode *info;

	gck_builder_add_ulong (&builder, CKA_CLASS, CKO_CERTIFICATE);
	gck_builder_add_ulong (&builder, CKA_CERTIFICATE_TYPE, CKC_X_509);
	gck_builder_add_string (&builder, CKA_VALUE, "invalid value");
	attributes = gck_attributes_ref_sink (gck_builder_end (&builder));

	object = g_object_new (mock_object_get_type (),
	                       "module", test->module,
	                       "session", test->session,
	                       "handle", INVALID,
	                       "attributes", attributes,
	                       NULL);

	gck_attributes_unref (attributes);

	info = _gcr_subject_public_key_load (object, NULL, &error);
	g_assert_error (error, GCK_ERROR, CKR_TEMPLATE_INCONSISTENT);
	g_assert (info == NULL);
	g_error_free (error);

	g_object_unref (object);
}

static const TestFixture FIXTURES[] = {
	{ "rsa", "client", 2048 },
	{ "dsa", "generic-dsa", 1024 },
};

static GPtrArray *test_names = NULL;

static const gchar *
test_name (const gchar *format,
           const gchar *basename)
{
	gchar *name = g_strdup_printf (format, basename);
	g_ptr_array_add (test_names, name);
	return name;
}

int
main (int argc, char **argv)
{
	const TestFixture *fixture;
	gint ret;
	guint i;

#if !GLIB_CHECK_VERSION(2,35,0)
	g_type_init ();
#endif
	g_test_init (&argc, &argv, NULL);

	test_names = g_ptr_array_new_with_free_func (g_free);

	for (i = 0; i < G_N_ELEMENTS (FIXTURES); i++) {
		fixture = &FIXTURES[i];

		g_test_add (test_name ("/gcr/subject-public-key/%s/cert-attributes", fixture->name), TestAttributes, fixture,
		            setup_attributes, test_for_cert_attributes, teardown_attributes);
		g_test_add (test_name ("/gcr/subject-public-key/%s/public-key-attributes", fixture->name), TestAttributes, fixture,
		            setup_attributes, test_for_public_key_attributes, teardown_attributes);
		g_test_add (test_name ("/gcr/subject-public-key/%s/private-key-attributes", fixture->name), TestAttributes, fixture,
		            setup_attributes, test_for_private_key_attributes, teardown_attributes);

		g_test_add (test_name ("/gcr/subject-public-key/%s/certificate-size", fixture->name), TestAttributes, fixture,
		            setup_attributes, test_certificate_calculate_size, teardown_attributes);
		g_test_add (test_name ("/gcr/subject-public-key/%s/public-key-size", fixture->name), TestAttributes, fixture,
		            setup_attributes, test_public_key_calculate_size, teardown_attributes);
		g_test_add (test_name ("/gcr/subject-public-key/%s/private-key-size", fixture->name), TestAttributes, fixture,
		            setup_attributes, test_private_key_calculate_size, teardown_attributes);

		g_test_add (test_name ("/gcr/subject-public-key/%s/certificate-load", fixture->name), TestLoading, fixture,
		            setup_loading, test_certificate_load, teardown_loading);
		g_test_add (test_name ("/gcr/subject-public-key/%s/public-key-load", fixture->name), TestLoading, fixture,
		            setup_loading, test_public_key_load, teardown_loading);
		g_test_add (test_name ("/gcr/subject-public-key/%s/private-key-load", fixture->name), TestLoading, fixture,
		            setup_loading, test_private_key_load, teardown_loading);

		g_test_add (test_name ("/gcr/subject-public-key/%s/certificate-load-async", fixture->name), TestLoading, fixture,
		            setup_loading, test_certificate_load_async, teardown_loading);
		g_test_add (test_name ("/gcr/subject-public-key/%s/public-key-load-async", fixture->name), TestLoading, fixture,
		            setup_loading, test_public_key_load_async, teardown_loading);
		g_test_add (test_name ("/gcr/subject-public-key/%s/private-key-load-async", fixture->name), TestLoading, fixture,
		            setup_loading, test_private_key_load_async, teardown_loading);

		g_test_add (test_name ("/gcr/subject-public-key/%s/certificate-load-already", fixture->name), TestLoading, fixture,
		            setup_loading, test_certificate_load_already, teardown_loading);
		g_test_add (test_name ("/gcr/subject-public-key/%s/public-key-load-already", fixture->name), TestLoading, fixture,
		            setup_loading, test_public_key_load_already, teardown_loading);
		g_test_add (test_name ("/gcr/subject-public-key/%s/private-key-load-already", fixture->name), TestLoading, fixture,
		            setup_loading, test_private_key_load_already, teardown_loading);

		g_test_add (test_name ("/gcr/subject-public-key/%s/certificate-load-partial", fixture->name), TestLoading, fixture,
		            setup_loading, test_certificate_load_partial, teardown_loading);
		g_test_add (test_name ("/gcr/subject-public-key/%s/public-key-load-partial", fixture->name), TestLoading, fixture,
		            setup_loading, test_public_key_load_partial, teardown_loading);
		g_test_add (test_name ("/gcr/subject-public-key/%s/private-key-load-partial", fixture->name), TestLoading, fixture,
		            setup_loading, test_private_key_load_partial, teardown_loading);
	}

	g_test_add ("/gcr/subject-public-key/load-failure-lookup", TestModule, NULL,
	            setup_module, test_load_failure_lookup, teardown_module);
	g_test_add ("/gcr/subject-public-key/load-failure-build", TestModule, NULL,
	            setup_module, test_load_failure_build, teardown_module);

	ret = egg_tests_run_with_loop ();

	g_ptr_array_free (test_names, TRUE);
	return ret;
}
