#include <array>
#include <gtest/gtest.h>
#include <support/exceptionAssertions.h>
#include <user/deviceAttachmentSpec.h>
#include <vector>

namespace smo {
namespace device {
namespace {

using ParamList = std::vector<std::pair<std::string, std::string>>;

ParamList makeParams(std::initializer_list<std::pair<const char*, const char*>> entries)
{
	ParamList params;
	for (const auto& entry : entries)
	{
		params.emplace_back(entry.first, entry.second);
	}

	return params;
}

TEST(DeviceAttachmentSpecNamesContainTest, MatchesVectorAndArraySynonyms)
{
	const std::vector<std::string> widthSynonyms = {
		"frame-width",
		"dim-w",
	};

	EXPECT_TRUE(DeviceAttachmentSpec::namesContain(widthSynonyms, "dim-w"));
	EXPECT_FALSE(DeviceAttachmentSpec::namesContain(widthSynonyms, "dim-h"));

	const std::array<std::string_view, 2> heightSynonyms = {
		"frame-height",
		"dim-h",
	};

	EXPECT_TRUE(DeviceAttachmentSpec::namesContain(heightSynonyms, "frame-height"));
	EXPECT_FALSE(DeviceAttachmentSpec::namesContain(heightSynonyms, "frame-width"));
}

TEST(DeviceAttachmentSpecParamsContainTest, DetectsExactParamName)
{
	const ParamList params = makeParams({
		{"display", "0"},
		{"screen", "1"},
	});

	EXPECT_TRUE(DeviceAttachmentSpec::paramsContain(params, "display"));
	EXPECT_TRUE(DeviceAttachmentSpec::paramsContain(params, "screen"));
	EXPECT_FALSE(DeviceAttachmentSpec::paramsContain(params, "width"));
}

TEST(DeviceAttachmentSpecParamNameMatchesAnySynonymGroupTest, MatchesAcrossGroups)
{
	const std::vector<std::string> widthSynonyms = {"dim-w", "frame-w"};
	const std::vector<std::string> heightSynonyms = {"dim-h", "frame-h"};

	EXPECT_TRUE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
		"dim-w", widthSynonyms, heightSynonyms));
	EXPECT_TRUE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
		"frame-h", widthSynonyms, heightSynonyms));
	EXPECT_FALSE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
		"colour-space", widthSynonyms, heightSynonyms));
}

TEST(DeviceAttachmentSpecNormalizeParamTokenTest, TrimsAndLowercases)
{
	EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("  TRUE  "), "true");
	EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("YUV"), "yuv");
	EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("480p"), "480p");
	EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken(""), "");
}

TEST(DeviceAttachmentSpecFindOptionalParamTest, LattermostExactNameWins)
{
	const ParamList params = makeParams({
		{"display", "0"},
		{"other", "ignored"},
		{"display", "2"},
	});

	const std::optional<std::pair<std::string, std::string>> matched =
		DeviceAttachmentSpec::findOptionalParam(params, "display");

	ASSERT_TRUE(matched.has_value());
	EXPECT_EQ(matched->first, "display");
	EXPECT_EQ(matched->second, "2");
}

TEST(DeviceAttachmentSpecFindOptionalParamWithSynonymsTest, LattermostSynonymWins)
{
	const std::vector<std::string> synonyms = {
		"cmd-timeout-ms",
		"command-timeout-ms",
	};

	const ParamList params = makeParams({
		{"cmd-timeout-ms", "5"},
		{"command-timeout-ms", "25"},
	});

	const std::optional<std::pair<std::string, std::string>> matched =
		DeviceAttachmentSpec::findOptionalParamWithSynonyms(params, synonyms);

	ASSERT_TRUE(matched.has_value());
	EXPECT_EQ(matched->first, "command-timeout-ms");
	EXPECT_EQ(matched->second, "25");
}

TEST(DeviceAttachmentSpecFindOptionalParamWithSynonymsTest, AbsentReturnsNullopt)
{
	const std::vector<std::string> synonyms = {"data-port"};
	const ParamList params = makeParams({{"cmd-port", "56001"}});

	EXPECT_FALSE(DeviceAttachmentSpec::findOptionalParamWithSynonyms(
		params, synonyms).has_value());
}

TEST(DeviceAttachmentSpecParseParamValueAsIntTest, ParsesValidInteger)
{
	EXPECT_EQ(
		DeviceAttachmentSpec::parseParamValueAsInt("width", "1280"),
		1280);
	EXPECT_EQ(
		DeviceAttachmentSpec::parseParamValueAsInt("width", "-42"),
		-42);
}

TEST(DeviceAttachmentSpecParseParamValueAsIntTest, InvalidIntegerThrows)
{
	try {
		DeviceAttachmentSpec::parseParamValueAsInt("width", "not-a-number");
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "Failed to parse 'width'");
	}
}

TEST(DeviceAttachmentSpecParseParamValueAsIntTest, OutOfRangeThrows)
{
	try {
		DeviceAttachmentSpec::parseParamValueAsInt(
			"width",
			"999999999999999999999");
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "out of range");
	}
}

TEST(DeviceAttachmentSpecParseParamValueAsPositiveIntTest, RejectsNonPositive)
{
	EXPECT_EQ(
		DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "640"),
		640);

	try {
		DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "0");
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "must be positive");
	}

	try {
		DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "-10");
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "must be positive");
	}
}

TEST(DeviceAttachmentSpecParseParamValueAsBoolTest, ParsesRecognizedValues)
{
	EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool(""));
	EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("true"));
	EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool(" YES "));
	EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("1"));
	EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("false"));
	EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("0"));
	EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("no"));
	EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool(
		"", /*emptyMeansTrue=*/false));
}

TEST(DeviceAttachmentSpecParseParamValueAsBoolTest, UnknownValueThrows)
{
	EXPECT_THROW(
		DeviceAttachmentSpec::parseParamValueAsBool("maybe"),
		std::runtime_error);
}

TEST(DeviceAttachmentSpecParseRequiredParamAsIntTest, MissingParamThrows)
{
	const ParamList params = makeParams({{"screen", "1"}});

	try {
		DeviceAttachmentSpec::parseRequiredParamAsInt(params, "display");
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "No display specified in params");
	}
}

TEST(DeviceAttachmentSpecParseRequiredParamAsIntTest, LattermostValueWins)
{
	const ParamList params = makeParams({
		{"display", "0"},
		{"display", "3"},
	});

	EXPECT_EQ(
		DeviceAttachmentSpec::parseRequiredParamAsInt(params, "display"),
		3);
}

TEST(DeviceAttachmentSpecParseOptionalParamAsIntTest, ReturnsDefaultWhenAbsent)
{
	const ParamList params = makeParams({{"screen", "1"}});

	EXPECT_EQ(
		DeviceAttachmentSpec::parseOptionalParamAsInt(params, "display", 42),
		42);
}

TEST(DeviceAttachmentSpecParseOptionalParamAsIntWithSynonymsTest, SynonymLattermostWins)
{
	const std::vector<std::string> synonyms = {
		"n-dgrams-per-frame",
		"num-dgrams-per-frame",
	};

	const ParamList params = makeParams({
		{"n-dgrams-per-frame", "10"},
		{"num-dgrams-per-frame", "99"},
	});

	EXPECT_EQ(
		DeviceAttachmentSpec::parseOptionalParamAsIntWithSynonyms(
			params, synonyms, 84),
		99);
}

TEST(DeviceAttachmentSpecParseRequiredParamAsIntWithSynonymsTest, ParsesMatchedSynonym)
{
	const std::vector<std::string> synonyms = {
		"cmd-timeout-ms",
		"command-timeout-ms",
	};

	const ParamList params = makeParams({
		{"command-timeout-ms", "15"},
	});

	EXPECT_EQ(
		DeviceAttachmentSpec::parseRequiredParamAsIntWithSynonyms(
			params,
			synonyms,
			"missing timeout"),
		15);
}

TEST(DeviceAttachmentSpecParseRequiredParamValueWithSynonymsTest, RequiresNonEmptyValue)
{
	const std::vector<std::string> synonyms = {"colour-space"};
	const ParamList missing = makeParams({{"width", "640"}});

	try {
		DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
			missing, synonyms, "colour-space required");
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "colour-space required");
	}

	const ParamList emptyValue = makeParams({{"colour-space", ""}});

	try {
		DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
			emptyValue, synonyms, "colour-space required");
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "colour-space required");
	}

	const ParamList present = makeParams({{"colour-space", "yuv"}});
	EXPECT_EQ(
		DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
			present, synonyms, "colour-space required"),
		"yuv");
}

TEST(DeviceAttachmentSpecParseOptionalParamValueWithSynonymsTest, OptionalStringValue)
{
	const std::vector<std::string> synonyms = {"colour-space"};
	const ParamList absent = makeParams({{"width", "640"}});

	EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
		absent, synonyms, "empty colour-space").has_value());

	const ParamList emptyValue = makeParams({{"colour-space", ""}});
	try {
		DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
			emptyValue, synonyms, "empty colour-space");
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "empty colour-space");
	}

	const ParamList present = makeParams({{"colour-space", "yuv"}});
	const std::optional<std::string> parsed =
		DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
			present, synonyms, "empty colour-space");

	ASSERT_TRUE(parsed.has_value());
	EXPECT_EQ(*parsed, "yuv");
}

TEST(DeviceAttachmentSpecParseOptionalParamAsBoolWithSynonymsTest, ParsesPresenceAndValues)
{
	const std::vector<std::string> synonyms = {
		"full-planar-is-optional",
		"opt-planar",
	};

	const ParamList absent = makeParams({{"width", "640"}});
	EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
		absent, synonyms, false));

	const ParamList presentEmpty = makeParams({{"opt-planar", ""}});
	EXPECT_TRUE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
		presentEmpty, synonyms, false));

	const ParamList presentFalse = makeParams({{"opt-planar", "false"}});
	EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
		presentFalse, synonyms, true));

	const ParamList lattermostWins = makeParams({
		{"opt-planar", "true"},
		{"full-planar-is-optional", "false"},
	});
	EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
		lattermostWins, synonyms, true));
}

TEST(DeviceAttachmentSpecRejectUnknownParamsTest, RejectsUnknownNames)
{
	const std::vector<std::string> knownA = {"width", "dim-w"};
	const std::vector<std::string> knownB = {"height", "dim-h"};
	const ParamList params = makeParams({
		{"dim-w", "640"},
		{"unknown-param", "1"},
	});

	try {
		DeviceAttachmentSpec::rejectUnknownParams(
			params,
			"unknown param '",
			knownA,
			knownB);
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception, "unknown-param");
	}
}

TEST(DeviceAttachmentSpecRejectUnknownParamsTest, AcceptsAllKnownNames)
{
	const std::vector<std::string> knownA = {"width", "dim-w"};
	const std::vector<std::string> knownB = {"height", "dim-h"};
	const ParamList params = makeParams({
		{"dim-w", "640"},
		{"dim-h", "480"},
	});

	EXPECT_NO_THROW(DeviceAttachmentSpec::rejectUnknownParams(
		params,
		"unknown param '",
		knownA,
		knownB));
}

} // namespace
} // namespace device
} // namespace smo
