/* Copyright 2016 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 *
 * Test Cr-50 Non-Voltatile memory module
 */

#include "common.h"
#include "console.h"
#include "crc.h"
#include "nvmem.h"
#include "flash.h"
#include "shared_mem.h"
#include "task.h"
#include "test_util.h"
#include "timer.h"
#include "util.h"

#define WRITE_SEGMENT_LEN 200
#define WRITE_READ_SEGMENTS 4

uint32_t nvmem_user_sizes[NVMEM_NUM_USERS] = {
	NVMEM_USER_0_SIZE,
	NVMEM_USER_1_SIZE,
	NVMEM_USER_2_SIZE
};

static uint8_t write_buffer[NVMEM_PARTITION_SIZE];
static uint8_t read_buffer[NVMEM_PARTITION_SIZE];
static int flash_write_fail;
static int lock_test_started;

int app_cipher(const void *salt_p, void *out_p, const void *in_p, size_t size)
{

	const uint8_t *in = in_p;
	uint8_t *out = out_p;
	const uint8_t *salt = salt_p;
	size_t i;

	for (i = 0; i < size; i++)
		out[i] = in[i] ^ salt[i % CIPHER_SALT_SIZE];

	return 1;
}

void app_compute_hash(uint8_t *p_buf, size_t num_bytes,
		      uint8_t *p_hash, size_t hash_bytes)
{
	uint32_t crc;
	uint32_t *p_data;
	int n;

	crc32_init();
	/* Assuming here that buffer is 4 byte aligned and that num_bytes is
	 * divisible by 4
	 */
	p_data = (uint32_t *)p_buf;
	for (n = 0; n < num_bytes/4; n++)
		crc32_hash32(*p_data++);
	crc = crc32_result();

	for (n = 0; n < hash_bytes; n += sizeof(crc)) {
		size_t copy_bytes = MIN(sizeof(crc), hash_bytes - n);

		memcpy(p_hash + n, &crc, copy_bytes);
	}
}

/* Used to allow/prevent Flash erase/write operations */
int flash_pre_op(void)
{
	return flash_write_fail ? EC_ERROR_UNKNOWN : EC_SUCCESS;
}

static int generate_random_data(int offset, int num_bytes)
{
	int m, n, limit;
	uint32_t r_data;

	/* Ensure it will fit in the write buffer */
	TEST_ASSERT((num_bytes + offset) <= NVMEM_PARTITION_SIZE);
	/* Seed random number sequence */
	r_data = prng((uint32_t)clock());
	m = 0;
	while (m < num_bytes) {
		r_data = prng(r_data);
		limit = MIN(4, num_bytes - m);
		/* No byte alignment assumptions */
		for (n = 0; n < limit; n++)
			write_buffer[offset + m + n] = (r_data >> (n*8)) & 0xff;
		m += limit;
	}

	return EC_SUCCESS;
}

static int test_write_read(uint32_t offset, uint32_t num_bytes, int user)
{
	int ret;

	/* Generate source data */
	generate_random_data(0, num_bytes);
	/* Write source data to NvMem */
	ret = nvmem_write(offset, num_bytes, write_buffer, user);
	/* Write to flash */
	ret = nvmem_commit();
	if (ret != EC_SUCCESS)
		return ret;
	/* Read from flash */
	nvmem_read(offset, num_bytes, read_buffer, user);
	/* Verify that write to flash was successful */
	TEST_ASSERT_ARRAY_EQ(write_buffer, read_buffer, num_bytes);

	return EC_SUCCESS;
}

static int write_full_buffer(uint32_t size, int user)
{
	uint32_t offset;
	uint32_t len;
	int ret;

	/* Start at beginning of the user buffer */
	offset = 0;
	do {
		/* User default segment length unless it will exceed */
		len = MIN(WRITE_SEGMENT_LEN, size - offset);
		/* Generate data for tx buffer */
		generate_random_data(offset, len);
		/* Write data to Nvmem cache memory */
		nvmem_write(offset, len, &write_buffer[offset], user);
		/* Write to flash */
		ret = nvmem_commit();
		if (ret != EC_SUCCESS)
			return ret;
		/* Adjust starting offset by segment length */
		offset += len;
	} while (offset < size);

	/* Entire flash buffer should be full at this point */
	nvmem_read(0, size, read_buffer, user);
	/* Verify that write to flash was successful */
	TEST_ASSERT_ARRAY_EQ(write_buffer, read_buffer, size);

	return EC_SUCCESS;
}

static int test_fully_erased_nvmem(void)
{
	/*
	 * The purpose of this test is to check NvMem intialization when NvMem
	 * is completely erased (i.e. following SpiFlash write of program). In
	 * this configuration, nvmem_init() should be able to detect this case
	 * and configure an initial NvMem partition.
	 */

	/* Erase full NvMem area */
	flash_physical_erase(CONFIG_FLASH_NVMEM_OFFSET_A,
			     NVMEM_PARTITION_SIZE);
	flash_physical_erase(CONFIG_FLASH_NVMEM_OFFSET_B,
			     NVMEM_PARTITION_SIZE);
	/* Call NvMem initialization function */
	return nvmem_init();
}

static int test_configured_nvmem(void)
{
	/*
	 * The purpose of this test is to check nvmem_init() when both
	 * partitions are configured and valid.
	 */

	/* Call NvMem initialization */
	return nvmem_init();
}

/* Verify that nvmem_erase_user_data only erases the given user's data. */
static int test_nvmem_erase_user_data(void)
{
	uint32_t write_value;
	uint32_t read_value;
	int i;

	nvmem_init();

	/* Make sure all partitions have data in them. */
	for (i = 0; i < NVMEM_NUM_PARTITIONS; i++) {
		write_value = i;
		nvmem_write(0, sizeof(write_value), &write_value, NVMEM_USER_0);
		write_value = 2;
		nvmem_write(0, sizeof(write_value), &write_value, NVMEM_USER_1);
		write_value = 3;
		nvmem_write(0, sizeof(write_value), &write_value, NVMEM_USER_2);
		nvmem_commit();
	}

	/* Check that the writes took place. */
	read_value = ~write_value;
	nvmem_read(0, sizeof(read_value), &read_value, NVMEM_USER_0);
	TEST_ASSERT(read_value == i-1);
	nvmem_read(0, sizeof(read_value), &read_value, NVMEM_USER_1);
	TEST_ASSERT(read_value == 2);
	nvmem_read(0, sizeof(read_value), &read_value, NVMEM_USER_2);
	TEST_ASSERT(read_value == 3);

	/*
	 * nvmem_erase_user_data() is supposed to erase the user's data across
	 * all partitions.
	 */
	nvmem_erase_user_data(NVMEM_USER_0);

	for (i = 0; i < NVMEM_NUM_PARTITIONS; i++) {
		/* Make sure USER 0's data is (still) gone. */
		nvmem_read(0, sizeof(read_value), &read_value, NVMEM_USER_0);
		TEST_ASSERT(read_value == 0xffffffff);

		/* Make sure the other users' data has been untouched. */
		nvmem_read(0, sizeof(read_value), &read_value, NVMEM_USER_1);
		TEST_ASSERT(read_value == 2);

		/*
		 * The active partition changes when the contents of the cache
		 * changes.  Therefore, in order to examine all the paritions,
		 * we'll keep modifying one of the user's data.
		 */
		nvmem_read(0, sizeof(read_value), &read_value, NVMEM_USER_2);
		TEST_ASSERT(read_value == (3+i));
		write_value = 4 + i;
		nvmem_write(0, sizeof(write_value), &write_value, NVMEM_USER_2);
		nvmem_commit();
	}

	return EC_SUCCESS;
}

static int test_corrupt_nvmem(void)
{
	uint8_t invalid_value = 0x55;
	int ret;
	struct nvmem_tag *p_part;
	uint8_t *p_data;

	/*
	 * The purpose of this test is to check nvmem_init() in the case when no
	 * vailid partition exists (not fully erased and no valid sha). In this
	 * case, the initialization create one new valid partition.
	 */

	/* Overwrite each partition will all 0s */
	memset(write_buffer, invalid_value, NVMEM_PARTITION_SIZE);
	flash_physical_write(CONFIG_FLASH_NVMEM_OFFSET_A,
				     NVMEM_PARTITION_SIZE,
				     (const char *)write_buffer);
	flash_physical_write(CONFIG_FLASH_NVMEM_OFFSET_B,
				     NVMEM_PARTITION_SIZE,
				     (const char *)write_buffer);
	/*
	 * The initialization function will look for a valid partition and if
	 * none is found, it will create one, and save it at partition index
	 * 1.
	 */
	ret = nvmem_init();
	if (ret)
		return ret;

	/*
	 * nvmem_init() called on uninitialized flash will create the first
	 * valid partition with generation set to 0 at flash partition 1.
	 *
	 * Check here that partition 1 has a generation number of 0.
	 */
	p_part = (struct nvmem_tag *)CONFIG_FLASH_NVMEM_BASE_B;
	TEST_ASSERT(p_part->generation == 0);
	p_data = (uint8_t *)p_part + sizeof(struct nvmem_tag);

	/* Verify that partition 0 is still empty. */
	memset(write_buffer, invalid_value, NVMEM_PARTITION_SIZE);
	p_data = (void *)CONFIG_FLASH_NVMEM_BASE_A;
	TEST_ASSERT_ARRAY_EQ(write_buffer, p_data, NVMEM_PARTITION_SIZE);

	/* Now let's write a different value  into user NVMEM_CR50 */
	invalid_value ^= ~0;
	TEST_ASSERT(nvmem_write(0, sizeof(invalid_value),
				&invalid_value, NVMEM_USER_0) == EC_SUCCESS);
	TEST_ASSERT(nvmem_commit() == EC_SUCCESS);

	/* Verify that partition 1 generation did not change. */
	TEST_ASSERT(p_part->generation == 0);

	/*
	 * Now verify that partition 0 generation is set to 1;
	 */
	p_part = (struct nvmem_tag *)CONFIG_FLASH_NVMEM_BASE_A;
	TEST_ASSERT(p_part->generation == 1);

	return EC_SUCCESS;
}

static int test_write_read_sequence(void)
{
	uint32_t offset;
	uint32_t length;
	int user;
	int n;
	int ret;

	for (user = 0; user < NVMEM_NUM_USERS; user++) {
		/* Length for each write/read segment */
		length = nvmem_user_sizes[user] / WRITE_READ_SEGMENTS;
		/* Start at beginning of user buffer */
		offset = 0;
		for (n = 0; n < WRITE_READ_SEGMENTS; n++) {
			ret = test_write_read(offset, length, user);
			if (ret != EC_SUCCESS)
				return ret;
			/* Adjust offset by segment length */
			offset += length;
			/* For 1st iteration only, adjust to create stagger */
			if (n == 0)
				offset -= length / 2;

		}
	}
	return EC_SUCCESS;
}

static int test_write_full_multi(void)
{
	int n;
	int ret;

	/*
	 * The purpose of this test is to completely fill each user buffer in
	 * NvMem with random data a segment length at a time. The data written
	 * to NvMem is saved in write_buffer[] and then can be used to check the
	 * NvMem writes were successful by reading and then comparing each user
	 * buffer.
	 */
	for (n = 0; n < NVMEM_NUM_USERS; n++) {
		ret = write_full_buffer(nvmem_user_sizes[n], n);
		if (ret != EC_SUCCESS)
			return ret;
	}
	return EC_SUCCESS;
}

static int test_write_fail(void)
{
	uint32_t offset = 0;
	uint32_t num_bytes = 0x200;
	int ret;

	/* Do write/read sequence that's expected to be successful */
	if (test_write_read(offset, num_bytes, NVMEM_USER_0))
		return EC_ERROR_UNKNOWN;

	/* Prevent flash erase/write operations */
	flash_write_fail = 1;
	/* Attempt flash write */
	ret = test_write_read(offset, num_bytes, NVMEM_USER_0);
	/* Resume normal operation */
	flash_write_fail = 0;

	/* This test is successful if write attempt failed */
	return !ret;
}

static int test_buffer_overflow(void)
{
	int ret;
	int n;

	/*
	 * The purpose of this test is to check that NvMem writes behave
	 * properly in relation to the defined length of each user buffer. A
	 * write operation to completely fill the buffer is done first. This
	 * should pass. Then the same buffer is written to with one extra byte
	 * and this operation is expected to fail.
	 */

	/* Do test for each user buffer */
	for (n = 0; n < NVMEM_NUM_USERS; n++) {
		/* Write full buffer */
		ret = write_full_buffer(nvmem_user_sizes[n], n);
		if (ret != EC_SUCCESS)
			return ret;
		/* Attempt to write full buffer plus 1 extra byte */
		ret = write_full_buffer(nvmem_user_sizes[n] + 1, n);
		if (!ret)
			return EC_ERROR_UNKNOWN;
	}

	/* Test case where user buffer number is valid */
	ret = test_write_read(0, 0x100, NVMEM_USER_0);
	if (ret != EC_SUCCESS)
		return ret;
	/* Attempt same write, but with invalid user number */
	ret = test_write_read(0, 0x100, NVMEM_NUM_USERS);
	if (!ret)
		return ret;

	return EC_SUCCESS;
}

static int test_move(void)
{
	uint32_t len = 0x100;
	uint32_t nv1_offset;
	uint32_t nv2_offset;
	int user = 0;
	int n;
	int ret;

	/*
	 * The purpose of this test is to check that nvmem_move() behaves
	 * properly. This test only uses one user buffer as accessing multiple
	 * user buffers is tested separately. This test uses writes a set of
	 * test data then test move operations with full overlap, half overlap
	 * and no overlap. Folliwng these tests, the boundary conditions for
	 * move operations are checked for the giver user buffer.
	 */

	nv1_offset = 0;
	for (n = 0; n < 3; n++) {
		/* Generate Test data */
		generate_random_data(nv1_offset, len);
		nv2_offset = nv1_offset + (len / 2) * n;
		/* Write data to Nvmem cache memory */
		nvmem_write(nv1_offset, len, &write_buffer[nv1_offset], user);
		nvmem_commit();
		/* Test move while data is in cache area */
		nvmem_move(nv1_offset, nv2_offset, len, user);
		nvmem_read(nv2_offset, len, read_buffer, user);
		if (memcmp(write_buffer, read_buffer, len))
			return EC_ERROR_UNKNOWN;
		ccprintf("Memmove nv1 = 0x%x, nv2 = 0x%x\n",
			 nv1_offset, nv2_offset);
	}
	/* Test invalid buffer offsets */
	/* Destination offset is equal to length of buffer */
	nv1_offset = 0;
	nv2_offset = nvmem_user_sizes[user];
	/* Attempt to move just 1 byte */
	ret = nvmem_move(nv1_offset, nv2_offset, 1, user);
	if (!ret)
		return EC_ERROR_UNKNOWN;

	/* Source offset is equal to length of buffer */
	nv1_offset = nvmem_user_sizes[user];
	nv2_offset = 0;
	/* Attempt to move just 1 byte */
	ret = nvmem_move(nv1_offset, nv2_offset, 1, user);
	if (!ret)
		return EC_ERROR_UNKNOWN;

	nv1_offset = 0;
	nv2_offset = nvmem_user_sizes[user] - len;
	/* Move data chunk from start to end of buffer */
	ret = nvmem_move(nv1_offset, nv2_offset,
			 len, user);
	if (ret)
		return ret;

	/* Attempt to move data chunk 1 byte beyond end of user buffer */
	nv1_offset = 0;
	nv2_offset = nvmem_user_sizes[user] - len + 1;
	ret = nvmem_move(nv1_offset, nv2_offset,
			 len, user);
	if (!ret)
		return EC_ERROR_UNKNOWN;
	/* nvmem_move returned an error, need to clear internal error state */
	nvmem_commit();

	return EC_SUCCESS;
}

static int test_is_different(void)
{
	uint32_t len = 0x41;
	uint32_t nv1_offset = 0;
	int user = 1;
	int ret;

	/*
	 * The purpose of this test is to verify nv_is_different(). Test data is
	 * written to a location in user buffer 1, then a case that's expected
	 * to pass along with a case that is expected to fail are checked. Next
	 * the same tests are repeated when the NvMem write is followed by a
	 * commit operation.
	 */

	/* Generate test data */
	generate_random_data(nv1_offset, len);
	/* Write to NvMem cache buffer */
	nvmem_write(nv1_offset, len, &write_buffer[nv1_offset], user);
	/* Expected to be the same */
	ret = nvmem_is_different(nv1_offset, len,
				 &write_buffer[nv1_offset], user);
	if (ret)
		return EC_ERROR_UNKNOWN;

	/* Expected to be different */
	ret = nvmem_is_different(nv1_offset + 1, len,
				 &write_buffer[nv1_offset], user);
	if (!ret)
		return EC_ERROR_UNKNOWN;

	/* Commit cache buffer and retest */
	nvmem_commit();
	/* Expected to be the same */
	ret = nvmem_is_different(nv1_offset, len,
				 &write_buffer[nv1_offset], user);
	if (ret)
		return EC_ERROR_UNKNOWN;

	/* Expected to be different */
	write_buffer[nv1_offset] ^= 0xff;
	ret = nvmem_is_different(nv1_offset, len,
				 &write_buffer[nv1_offset], user);
	if (!ret)
		return EC_ERROR_UNKNOWN;

	return EC_SUCCESS;
}

int nvmem_first_task(void *unused)
{
	uint32_t offset = 0;
	uint32_t num_bytes = WRITE_SEGMENT_LEN;
	int user = NVMEM_USER_0;

	task_wait_event(0);
	/* Generate source data */
	generate_random_data(0, num_bytes);
	nvmem_write(0, num_bytes, &write_buffer[offset], user);
	/* Read from cache memory */
	nvmem_read(0, num_bytes, read_buffer, user);
	/* Verify that write to nvmem was successful */
	TEST_ASSERT_ARRAY_EQ(write_buffer, read_buffer, num_bytes);
	/* Wait here with mutex held by this task */
	task_wait_event(0);
	/* Write to flash which releases nvmem mutex */
	nvmem_commit();
	nvmem_read(0, num_bytes, read_buffer, user);
	/* Verify that write to flash was successful */
	TEST_ASSERT_ARRAY_EQ(write_buffer, read_buffer, num_bytes);

	return EC_SUCCESS;
}

int nvmem_second_task(void *unused)
{
	uint32_t offset = WRITE_SEGMENT_LEN;
	uint32_t num_bytes = WRITE_SEGMENT_LEN;
	int user = NVMEM_USER_0;

	task_wait_event(0);

	/* Gen test data and don't overwite test data generated by 1st task */
	generate_random_data(offset, num_bytes);
	/* Write test data at offset 0 nvmem user buffer */
	nvmem_write(0, num_bytes, &write_buffer[offset], user);
	/* Write to flash */
	nvmem_commit();
	/* Read from nvmem */
	nvmem_read(0, num_bytes, read_buffer, user);
	/* Verify that write to nvmem was successful */
	TEST_ASSERT_ARRAY_EQ(&write_buffer[offset], read_buffer, num_bytes);
	/* Clear flag to indicate lock test is complete */
	lock_test_started = 0;

	return EC_SUCCESS;
}

static int test_lock(void)
{
	/*
	 * This purpose of this test is to verify the mutex lock portion of the
	 * nvmem module. There are two additional tasks utilized. The first task
	 * is woken and it creates some test data and does an
	 * nvmem_write(). This will cause the mutex to be locked by the 1st
	 * task. The 1st task then waits and control is returned to this
	 * function and the 2nd task is woken, the 2nd task also attempts to
	 * write data to nvmem. The 2nd task should stall waiting for the mutex
	 * to be unlocked.
	 *
	 * When control returns to this function, the 1st task is woken again
	 * and the nvmem operation is completed. This will allow the 2nd task to
	 * grab the lock and finish its nvmem operation. The test will not
	 * complete until the 2nd task finishes the nvmem write. A static global
	 * flag is used to let this function know when the 2nd task is complete.
	 *
	 * Both tasks write to the same location in nvmem so the test will only
	 * pass if the 2nd task can't write until the nvmem write in the 1st
	 * task is completed.
	 */

	/* Set flag for start of test */
	lock_test_started = 1;
	/* Wake first_task */
	task_wake(TASK_ID_NV_1);
	task_wait_event(1000);
	/* Wake second_task. It should stall waiting for mutex */
	task_wake(TASK_ID_NV_2);
	task_wait_event(1000);
	/* Go back to first_task so it can complete its nvmem operation */
	task_wake(TASK_ID_NV_1);
	/* Wait for 2nd task to complete nvmem operation */
	while (lock_test_started)
		task_wait_event(100);

	return EC_SUCCESS;
}

static int test_nvmem_save(void)
{
	/*
	 * The purpose of this test is to verify that if the written value
	 * did not change the cache contents there is no actual write
	 * happening at the commit time.
	 */
	int dummy_value;
	int offset = 0x10;
	uint8_t generation_a;
	uint8_t generation_b;
	uint8_t prev_generation;
	uint8_t new_generation;
	const struct nvmem_tag *part_a;
	const struct nvmem_tag *part_b;
	const struct nvmem_tag *new_gen_part;
	const struct nvmem_tag *prev_gen_part;

	part_a = (const struct nvmem_tag *)CONFIG_FLASH_NVMEM_BASE_A;
	part_b = (const struct nvmem_tag *)CONFIG_FLASH_NVMEM_BASE_B;
	/*
	 * Make sure nvmem is initialized and both partitions have been
	 * written.
	 */
	nvmem_init();

	/*
	 * Make sure something is changed at offset 0x10 into the second user
	 * space.
	 */
	nvmem_read(offset, sizeof(dummy_value), &dummy_value, NVMEM_USER_1);
	dummy_value ^= ~0;
	nvmem_write(0x10, sizeof(dummy_value), &dummy_value, NVMEM_USER_1);
	nvmem_commit();

	/* Verify that the two generation values are different. */
	generation_a = part_a->generation;
	generation_b = part_b->generation;
	TEST_ASSERT(generation_a != generation_b);

	/*
	 * Figure out which one should change next, we are close to the
	 * beginnig of the test, no wrap is expected.
	 */
	if (generation_a > generation_b) {
		prev_generation = generation_a;
		new_generation = generation_a + 1;
		new_gen_part = part_b;
		prev_gen_part = part_a;
	} else {
		prev_generation = generation_b;
		new_generation = generation_b + 1;
		new_gen_part = part_a;
		prev_gen_part = part_b;
	}

	/* Write a new value, this should trigger generation switch. */
	dummy_value += 1;
	TEST_ASSERT(nvmem_write(0x10, sizeof(dummy_value),
				&dummy_value, NVMEM_USER_1) == EC_SUCCESS);
	TEST_ASSERT(nvmem_commit() == EC_SUCCESS);

	TEST_ASSERT(prev_gen_part->generation == prev_generation);
	TEST_ASSERT(new_gen_part->generation == new_generation);

	/* Write the same value, this should NOT trigger generation switch. */
	TEST_ASSERT(nvmem_write(0x10, sizeof(dummy_value),
				&dummy_value, NVMEM_USER_1) == EC_SUCCESS);
	TEST_ASSERT(nvmem_commit() == EC_SUCCESS);

	TEST_ASSERT(prev_gen_part->generation == prev_generation);
	TEST_ASSERT(new_gen_part->generation == new_generation);

	return EC_SUCCESS;
}

static void run_test_setup(void)
{
	/* Allow Flash erase/writes */
	flash_write_fail = 0;
	test_reset();
}

void run_test(void)
{
	run_test_setup();
	RUN_TEST(test_corrupt_nvmem);
	RUN_TEST(test_fully_erased_nvmem);
	RUN_TEST(test_configured_nvmem);
	RUN_TEST(test_write_read_sequence);
	RUN_TEST(test_write_full_multi);
	RUN_TEST(test_write_fail);
	RUN_TEST(test_buffer_overflow);
	RUN_TEST(test_move);
	RUN_TEST(test_is_different);
	RUN_TEST(test_lock);
	RUN_TEST(test_nvmem_erase_user_data);
	RUN_TEST(test_nvmem_save);
	test_print_result();
}
