// SPDX-License-Identifier: LGPL-2.1-or-later

#include <errno.h>
#include <stdlib.h>
#include <string.h>

#include <ccan/array_size/array_size.h>
#include <ccan/endian/endian.h>

#include <libnvme.h>
#include <nvme/private.h>

#include "mock.h"
#include "util.h"

#define TEST_FD 0xFD
#define HEADER_LEN 20

static void arbitrary_ascii_string(size_t max_len, char *str, char *log_str)
{
	size_t len;
	size_t i;

	len = arbitrary_range(max_len + 1);
	for (i = 0; i < len; i++) {
		/*
		 * ASCII strings shall contain only code values 20h through 7Eh.
		 * Exclude 20h (space) because it ends the string.
		 */
		str[i] = log_str[i] = arbitrary_range(0x7E - 0x20) + 0x20 + 1;
	}
	for (i = len; i < max_len; i++) {
		str[i] = '\0';
		log_str[i] = ' ';
	}
}

static void arbitrary_entry(struct nvmf_disc_log_entry *entry,
                            struct nvmf_disc_log_entry *log_entry)
{
	arbitrary(entry, sizeof(*entry));
	memcpy(log_entry, entry, sizeof(*entry));
	arbitrary_ascii_string(
		sizeof(entry->trsvcid), entry->trsvcid, log_entry->trsvcid);
	arbitrary_ascii_string(
		sizeof(entry->traddr), entry->traddr, log_entry->traddr);
}

static void arbitrary_entries(size_t len,
                              struct nvmf_disc_log_entry *entries,
                              struct nvmf_disc_log_entry *log_entries)
{
	size_t i;

	for (i = 0; i < len; i++)
		arbitrary_entry(&entries[i], &log_entries[i]);
}

static void test_no_entries(nvme_ctrl_t c)
{
	struct nvmf_discovery_log header = {};
	/* No entries to fetch after fetching the header */
	struct mock_cmd mock_admin_cmds[] = {
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header,
		},
	};
	struct nvmf_discovery_log *log = NULL;

	set_mock_admin_cmds(mock_admin_cmds, ARRAY_SIZE(mock_admin_cmds));
	check(nvmf_get_discovery_log(c, &log, 1) == 0, "discovery failed: %m");
	end_mock_cmds();
	cmp(log, &header, sizeof(header), "incorrect header");
	free(log);
}

static void test_four_entries(nvme_ctrl_t c)
{
	size_t num_entries = 4;
	struct nvmf_disc_log_entry entries[num_entries];
	struct nvmf_disc_log_entry log_entries[num_entries];
	struct nvmf_discovery_log header = {.numrec = cpu_to_le64(num_entries)};
	/*
	 * All 4 entries should be fetched at once
	 * followed by the header again (to ensure genctr hasn't changed)
	 */
	struct mock_cmd mock_admin_cmds[] = {
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = sizeof(entries),
			.cdw10 = (sizeof(entries) / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.cdw12 = sizeof(header), /* LPOL */
			.out_data = log_entries,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header,
		},
	};
	struct nvmf_discovery_log *log = NULL;

	arbitrary_entries(num_entries, entries, log_entries);
	set_mock_admin_cmds(mock_admin_cmds, ARRAY_SIZE(mock_admin_cmds));
	check(nvmf_get_discovery_log(c, &log, 1) == 0, "discovery failed: %m");
	end_mock_cmds();
	cmp(log, &header, sizeof(header), "incorrect header");
	cmp(log->entries, entries, sizeof(entries), "incorrect entries");
	free(log);
}

static void test_five_entries(nvme_ctrl_t c)
{
	size_t num_entries = 5;
	struct nvmf_disc_log_entry entries[num_entries];
	struct nvmf_disc_log_entry log_entries[num_entries];
	size_t first_entries = 4;
	size_t first_data_len = first_entries * sizeof(*entries);
	size_t second_entries = num_entries - first_entries;
	size_t second_data_len = second_entries * sizeof(*entries);
	struct nvmf_discovery_log header = {.numrec = cpu_to_le64(num_entries)};
	/*
	 * The first 4 entries (4 KB) are fetched together,
	 * followed by last entry separately.
	 * Finally, the header is fetched again to check genctr.
	 */
	struct mock_cmd mock_admin_cmds[] = {
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = first_data_len,
			.cdw10 = (first_data_len / 4 - 1) << 16 /* NUMDL */
			       | 1 << 15 /* RAE */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.cdw12 = sizeof(header), /* LPOL */
			.out_data = log_entries,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = second_data_len,
			.cdw10 = (second_data_len / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.cdw12 = sizeof(header) + first_data_len, /* LPOL */
			.out_data = log_entries + first_entries,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header,
		},
	};
	struct nvmf_discovery_log *log = NULL;

	arbitrary_entries(num_entries, entries, log_entries);
	set_mock_admin_cmds(mock_admin_cmds, ARRAY_SIZE(mock_admin_cmds));
	check(nvmf_get_discovery_log(c, &log, 1) == 0, "discovery failed: %m");
	end_mock_cmds();
	cmp(log, &header, sizeof(header), "incorrect header");
	cmp(log->entries, entries, sizeof(entries), "incorrect entries");
	free(log);
}

static void test_genctr_change(nvme_ctrl_t c)
{
	struct nvmf_disc_log_entry entries1[1];
	struct nvmf_discovery_log header1 = {
		.numrec = cpu_to_le64(ARRAY_SIZE(entries1)),
	};
	size_t num_entries2 = 2;
	struct nvmf_disc_log_entry entries2[num_entries2];
	struct nvmf_disc_log_entry log_entries2[num_entries2];
	struct nvmf_discovery_log header2 = {
		.genctr = cpu_to_le64(1),
		.numrec = cpu_to_le64(num_entries2),
	};
	/*
	 * genctr changes after the entries are fetched the first time,
	 * so the log page entries are refetched
	 */
	struct mock_cmd mock_admin_cmds[] = {
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header1,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = sizeof(entries1),
			.cdw10 = (sizeof(entries1) / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* NUMDL */
			.cdw12 = sizeof(header1), /* LPOL */
			.out_data = entries1,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header2,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = sizeof(entries2),
			.cdw10 = (sizeof(entries2) / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.cdw12 = sizeof(header2), /* LPOL */
			.out_data = log_entries2,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header2,
		},
	};
	struct nvmf_discovery_log *log = NULL;

	arbitrary(entries1, sizeof(entries1));
	arbitrary_entries(num_entries2, entries2, log_entries2);
	set_mock_admin_cmds(mock_admin_cmds, ARRAY_SIZE(mock_admin_cmds));
	check(nvmf_get_discovery_log(c, &log, 2) == 0, "discovery failed: %m");
	end_mock_cmds();
	cmp(log, &header2, sizeof(header2), "incorrect header");
	cmp(log->entries, entries2, sizeof(entries2), "incorrect entries");
	free(log);
}

static void test_max_retries(nvme_ctrl_t c)
{
	struct nvmf_disc_log_entry entry;
	struct nvmf_discovery_log header1 = {.numrec = cpu_to_le64(1)};
	struct nvmf_discovery_log header2 = {
		.genctr = cpu_to_le64(1),
		.numrec = cpu_to_le64(1),
	};
	struct nvmf_discovery_log header3 = {
		.genctr = cpu_to_le64(2),
		.numrec = cpu_to_le64(1),
	};
	/* genctr changes in both attempts, hitting the max retries (2) */
	struct mock_cmd mock_admin_cmds[] = {
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header1,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = sizeof(entry),
			.cdw10 = (sizeof(entry) / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.cdw12 = sizeof(header1), /* LPOL */
			.out_data = &entry,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header2,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = sizeof(entry),
			.cdw10 = (sizeof(entry) / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.cdw12 = sizeof(header2), /* LPOL */
			.out_data = &entry,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header3,
		},
	};
	struct nvmf_discovery_log *log = NULL;

	arbitrary(&entry, sizeof(entry));
	set_mock_admin_cmds(mock_admin_cmds, ARRAY_SIZE(mock_admin_cmds));
	check(nvmf_get_discovery_log(c, &log, 2) == -1, "discovery succeeded");
	end_mock_cmds();
	check(errno == EAGAIN, "discovery failed: %m");
	check(!log, "unexpected log page returned");
}

static void test_header_error(nvme_ctrl_t c)
{
	/* Stop after an error in fetching the header the first time */
	struct mock_cmd mock_admin_cmds[] = {
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.err = NVME_SC_INVALID_OPCODE,
		},
	};
	struct nvmf_discovery_log *log = NULL;

	set_mock_admin_cmds(mock_admin_cmds, ARRAY_SIZE(mock_admin_cmds));
	check(nvmf_get_discovery_log(c, &log, 1) == -1, "discovery succeeded");
	end_mock_cmds();
	check(!log, "unexpected log page returned");
}

static void test_entries_error(nvme_ctrl_t c)
{
	struct nvmf_discovery_log header = {.numrec = cpu_to_le64(1)};
	size_t entry_size = sizeof(struct nvmf_disc_log_entry);
	/* Stop after an error in fetching the entries */
	struct mock_cmd mock_admin_cmds[] = {
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = entry_size,
			.cdw10 = (entry_size / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.cdw12 = sizeof(header), /* LPOL */
			.err = -EIO,
		},
	};
	struct nvmf_discovery_log *log = NULL;

	set_mock_admin_cmds(mock_admin_cmds, ARRAY_SIZE(mock_admin_cmds));
	check(nvmf_get_discovery_log(c, &log, 1) == -1, "discovery succeeded");
	end_mock_cmds();
	check(errno == EIO, "discovery failed: %m");
	check(!log, "unexpected log page returned");
}

static void test_genctr_error(nvme_ctrl_t c)
{
	struct nvmf_disc_log_entry entry;
	struct nvmf_discovery_log header = {.numrec = cpu_to_le64(1)};
	/* Stop after an error in refetching the header */
	struct mock_cmd mock_admin_cmds[] = {
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.out_data = &header,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = sizeof(entry),
			.cdw10 = (sizeof(entry) / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.cdw12 = sizeof(header), /* LPOL */
			.out_data = &entry,
		},
		{
			.opcode = nvme_admin_get_log_page,
			.data_len = HEADER_LEN,
			.cdw10 = (HEADER_LEN / 4 - 1) << 16 /* NUMDL */
			       | NVME_LOG_LID_DISCOVER, /* LID */
			.err = NVME_SC_INTERNAL,
		},
	};
	struct nvmf_discovery_log *log = NULL;

	arbitrary(&entry, sizeof(entry));
	set_mock_admin_cmds(mock_admin_cmds, ARRAY_SIZE(mock_admin_cmds));
	check(nvmf_get_discovery_log(c, &log, 1) == -1, "discovery succeeded");
	end_mock_cmds();
	check(!log, "unexpected log page returned");
}

static void run_test(const char *test_name, void (*test_fn)(nvme_ctrl_t))
{
	struct nvme_ctrl c = {.fd = TEST_FD};

	printf("Running test %s...", test_name);
	fflush(stdout);
	check(asprintf(&c.name, "%s_ctrl", test_name) >= 0, "asprintf() failed");
	test_fn(&c);
	free(c.name);
	puts(" OK");
}

#define RUN_TEST(name) run_test(#name, test_ ## name)

int main(void)
{
	set_mock_fd(TEST_FD);
	RUN_TEST(no_entries);
	RUN_TEST(four_entries);
	RUN_TEST(five_entries);
	RUN_TEST(genctr_change);
	RUN_TEST(max_retries);
	RUN_TEST(header_error);
	RUN_TEST(entries_error);
	RUN_TEST(genctr_error);
}
