#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include "cmockery.h"

#define free our_free

static void our_free(void* ptr);

#include "../memprot.c"

static bool is_assert_checking =
#ifdef USE_ASSERT_CHECKING
		true;
#else
		false;
#endif

/* copy of gp_max_alloc_size_mb */
static bool our_gp_max_alloc_size_mb = 2048;

#define EXPECT_EXCEPTION()     \
	expect_any(ExceptionalCondition,conditionName); \
	expect_any(ExceptionalCondition,errorType); \
	expect_any(ExceptionalCondition,fileName); \
	expect_any(ExceptionalCondition,lineNumber); \
    will_be_called_with_sideeffect(ExceptionalCondition, &_ExceptionalCondition, NULL);\

#undef PG_RE_THROW
#define PG_RE_THROW() siglongjmp(*PG_exception_stack, 1)

/*
 * Saves the expected input of our_free function to check if we correctly free
 * vmem pointer, instead of the user pointer
 */
static void* our_free_expected_pointer = NULL;
/* Facilitates verification that gp_free actually called free */
static void* our_free_input_pointer = NULL;

/*
 * An overriden method for "free" (using macro substitution) to verify if free was called
 * with proper vmem pointer
 */
static void our_free(void* ptr)
{
	our_free_input_pointer = ptr;
	assert_true(ptr == our_free_expected_pointer);
}

/*
 * This method will emulate the real ExceptionalCondition
 * function by re-throwing the exception, essentially falling
 * back to the next available PG_CATCH();
 */
static void
_ExceptionalCondition()
{
     PG_RE_THROW();
}

/*
 * This method sets up memprot.
 */
static void
MemProtTestSetup(void **state)
{
	gp_mp_inited = true;
	memprotOwnerThread = pthread_self();
}

/*
 * This method resets memprot state.
 */
static void
MemProtTestTeardown(void **state)
{
	gp_mp_inited = false;
}

static size_t CalculateVmemSizeFromUserSize(size_t user_size)
{
	size_t chosen_vmem_size = sizeof(VmemHeader) + user_size;

#ifdef GP_ALLOC_DEBUG
	chosen_vmem_size += sizeof(FooterChecksumType);
#endif

	return chosen_vmem_size;
}

/*
 * This method calculates a new size of a pointer given a ratio to the original size.
 * The returned size will be between the maximum requestable size and zero
 */
static size_t CalculateReallocSize(size_t original_size, float ratio)
{
	assert_true(ratio >= 0.0);

	size_t new_size = ((float)original_size) * ratio;

	/* Overflow or exceeding the max allowed allocation size */
	if ((ratio > 1.0 && new_size < original_size) || (new_size > our_gp_max_alloc_size_mb * 1024L * 1024L))
	{
		return our_gp_max_alloc_size_mb * 1024L * 1024L;
	}

	return new_size;
}

/* Verifies that the stored size is same as the expected size of the usable memory */
static void VerifyStoredSize(void* ptr, size_t expected_user_size)
{
	size_t stored_size = *((size_t *)((((char *) ptr) - sizeof(VmemHeader)) + offsetof(VmemHeader, size)));
	assert_true(stored_size == expected_user_size);
}

/* Wrapper around gp_malloc that executes basic sets of tests during allocations */
static void* AllocateWithCheck(size_t size)
{
	size_t chosen_vmem_size = CalculateVmemSizeFromUserSize(size);

	/* Too big allocation should fail assert checking */
	if (is_assert_checking && chosen_vmem_size > our_gp_max_alloc_size_mb * 1024L * 1024L)
	{
		EXPECT_EXCEPTION();
	}
	else
	{
		/* The expect_value's chosen_vmem_size will check the size calculation with overhead */
		expect_value(VmemTracker_ReserveVmem, newlyRequestedBytes, chosen_vmem_size);
		will_return(VmemTracker_ReserveVmem, MemoryAllocation_Success);
	}

	PG_TRY();
	{
		void *ptr = gp_malloc(size);

		/* size limit is only checked in assert build */
		if (is_assert_checking && chosen_vmem_size > our_gp_max_alloc_size_mb * 1024L * 1024L)
		{
			assert_true(false);
		}

		size_t stored_size = *((size_t *)((((char *) ptr) - sizeof(VmemHeader)) + offsetof(VmemHeader, size)));
		assert_true(stored_size == size);

		return ptr;
	}
	PG_CATCH();
	{
	}
	PG_END_TRY();

	return NULL;
}

/*
 * Calls gp_realloc, testing that the pointer is properly resized. Returns
 * the newly resized pointer.
 */
static void* ReallocateWithCheck(void* ptr, size_t requested_size)
{
	/*
	 * Nothing to check for a null pointer in optimized build as we will just crash
	 * and this is consistent with existing behavior of repalloc.
	 */
	if (NULL == ptr)
	{
		if (is_assert_checking)
		{
			/* The gp_realloc should fail assert for null pointer check */
			EXPECT_EXCEPTION();
			PG_TRY();
			{
				gp_realloc(ptr, requested_size);
				assert_true(false);
			}
			PG_CATCH();
			{

			}
			PG_END_TRY();
		}

		/*
		 * For release build or for debug build after failing the assertion in gp_realloc,
		 * we don't have any further execution to check
		 */
		return NULL;
	}

	size_t orig_user_size = UserPtr_GetUserPtrSize(ptr);
	size_t orig_vmem_size = CalculateVmemSizeFromUserSize(orig_user_size);
	size_t size_difference = requested_size - orig_user_size;

	/* If the size change is negative, we release vmem */
	if (requested_size > orig_user_size)
	{
		expect_value(VmemTracker_ReserveVmem, newlyRequestedBytes, requested_size - orig_user_size);
		will_return(VmemTracker_ReserveVmem, MemoryAllocation_Success);
	}
	else if (requested_size < orig_user_size)
	{
		/* ReleaseVmem will release the size difference */
		expect_value(VmemTracker_ReleaseVmem, toBeFreedRequested, abs(size_difference));
		will_be_called(VmemTracker_ReleaseVmem);
	}

	void *realloc_ptr =	gp_realloc(ptr, requested_size);
	assert_true(ptr == realloc_ptr || requested_size != orig_user_size);

	assert_true(requested_size == UserPtr_GetUserPtrSize(realloc_ptr));

	/* Check vmem size has been recalculated */
	size_t realloc_vmem_size = orig_vmem_size + size_difference;
	assert_true(realloc_vmem_size == UserPtr_GetVmemPtrSize(realloc_ptr));

	return realloc_ptr;
}

/*
 * Frees a user pointer, checking against the original user size
 */
static void FreeWithCheck(void* ptr, size_t size)
{
	size_t vmem_size = 0;

	/*
	 * Nothing to check for a null pointer in optimized build as we will just crash
	 * and this is consistent with existing behavior of pfree.
	 */
	if (NULL == ptr)
	{
		if (is_assert_checking)
		{
			/* The gp_free should fail assert for null pointer check */
			EXPECT_EXCEPTION();
			PG_TRY();
			{
				gp_free(ptr);
				assert_true(false);
			}
			PG_CATCH();
			{

			}
			PG_END_TRY();
		}

		/*
		 * For release build or for debug build after failing the assertion in gp_free,
		 * we don't have any further execution to check
		 */
		return;
	}

	size_t stored_size = UserPtr_GetUserPtrSize(ptr);
	assert_true(stored_size == size);

	vmem_size = CalculateVmemSizeFromUserSize(size);
	assert_true(vmem_size ==  UserPtr_GetVmemPtrSize(ptr));

	our_free_expected_pointer = ((char*)ptr - sizeof(VmemHeader));

	/* Set the our_free_input_pointer to null to detect if free was actually called */
	our_free_input_pointer = NULL;

	if (!is_assert_checking || size != 0)
	{
		/* The expect_value's chosen_vmem_size will check the size calculation with overhead */
		expect_value(VmemTracker_ReleaseVmem, toBeFreedRequested, vmem_size);
		will_be_called(VmemTracker_ReleaseVmem);
	}

	/*
	 * Verify that the input pointer (supposed to be saved inside our_free to indicate
	 * that it was called) and the expected pointer are not same, before calling gp_free
	 */
	assert_true(our_free_expected_pointer != our_free_input_pointer);

	if (is_assert_checking && size == 0)
	{
		EXPECT_EXCEPTION();
	}

	PG_TRY();
	{
		gp_free(ptr);

		if (size == 0)
		{
			assert_true(!is_assert_checking || false);
		}

		/* For non-zero size, we expect the free (and our_free) to be called */
		assert_true(size == 0 || our_free_input_pointer == our_free_expected_pointer);
	}
	PG_CATCH();
	{

	}
	PG_END_TRY();
}

/* Checks if we bypass vmem tracker when mp_init is false */
static void
test__gp_malloc_no_vmem_tracker_when_mp_init_false(void **state)
{
	gp_mp_inited = false;

	const size_t alloc_size = 10;
	/* No expected calls to VMEM tracker as the mp is not initialized */
	void * alloc = gp_malloc(alloc_size);
	assert_true(alloc != NULL);
	VerifyStoredSize(alloc, alloc_size);
}

/* Checks if we call vmem tracker when mp_init is true */
static void
test__gp_malloc_calls_vmem_tracker_when_mp_init_true(void **state)
{
	gp_mp_inited = true;
	const size_t alloc_size = 10;

	expect_value(VmemTracker_ReserveVmem, newlyRequestedBytes, CalculateVmemSizeFromUserSize(alloc_size));
	will_return(VmemTracker_ReserveVmem, MemoryAllocation_Success);

	/* No expected calls to VMEM tracker as the mp is not initialized */
	void * alloc = gp_malloc(alloc_size);
	assert_true(alloc != NULL);
	VerifyStoredSize(alloc, alloc_size);
}

/*
 * Tests the basic functionality of gp_malloc and gp_free
 */
static void
test__gp_malloc_and_free__basic_tests(void **state)
{
	size_t sizes[] = {50, 1024, our_gp_max_alloc_size_mb * 1024L * 1024L - sizeof(VmemHeader) - FOOTER_CHECKSUM_SIZE,
			1024L * 1024L * 1024L * 2L, 1024L * 1024L * 1024L * 5L};

	for (int idx = 0; idx < sizeof(sizes)/sizeof(sizes[0]); idx++)
	{
		size_t chosen_size = sizes[idx];
		void *ptr = AllocateWithCheck(chosen_size);
		FreeWithCheck(ptr, chosen_size);
	}
}

/*
 * Tests the basic functionality of gp_realloc
 */
static void
test__gp_realloc__basic_tests(void **state)
{
	size_t sizes[] = {50, 1024, our_gp_max_alloc_size_mb * 1024L * 1024L - sizeof(VmemHeader) - FOOTER_CHECKSUM_SIZE};
	/* Ratio of new size to original size for realloc calls */
	float fractions[] = {0, 0.1, 0.5, 1, 1.5, 2};

	for (int idx = 0; idx < sizeof(sizes)/sizeof(sizes[0]); idx++)
	{
		for (int didx = 0; didx < sizeof(fractions)/sizeof(fractions[0]); didx++)
		{
			size_t original_size = sizes[idx];
			float chosen_fraction = fractions[didx];
			size_t requested_size = CalculateReallocSize(original_size, chosen_fraction);

			void *ptr = AllocateWithCheck(original_size);
			ptr = ReallocateWithCheck(ptr, requested_size);
			FreeWithCheck(ptr, requested_size);
		}
	}
}

int
main(int argc, char* argv[])
{
	cmockery_parse_arguments(argc, argv);

	const UnitTest tests[] = {
		unit_test_setup_teardown(test__gp_malloc_no_vmem_tracker_when_mp_init_false, MemProtTestSetup, MemProtTestTeardown),
		unit_test_setup_teardown(test__gp_malloc_calls_vmem_tracker_when_mp_init_true, MemProtTestSetup, MemProtTestTeardown),
		unit_test_setup_teardown(test__gp_malloc_and_free__basic_tests, MemProtTestSetup, MemProtTestTeardown),
		unit_test_setup_teardown(test__gp_realloc__basic_tests, MemProtTestSetup, MemProtTestTeardown),
	};

	return run_tests(tests);
}
