#include <gtest/gtest.h>
#include <catalogHelpers.h>
#include <selectorParse.h>
#include <selectorResolve.h>
#include <stdexcept>
#include <string>
#include <vector>

namespace lcamera_dev {
namespace {

constexpr const char *dellLaptopMachineTag = "dell-laptop";

static CameraIdentityRecord makeRecord(
	const std::string& id,
	const std::string& model = "",
	const std::string& locationLabel = "")
{
	CameraIdentityRecord record;
	record.id = id;
	record.model = model;
	record.locationLabel = locationLabel;
	return record;
}

static std::vector<CameraIdentityRecord> syntheticAmbiguityRecords()
{
	return {
		makeRecord("/base/cam0", "imx219", "back"),
		makeRecord("/base/cam1", "Logitech C920", "external"),
		makeRecord("/base/cam2", "imx219", "front"),
	};
}

TEST(FormatCameraListForDiagnosticsTest, ListsIndexedCameras)
{
	const std::vector<CameraIdentityRecord> records =
		tests::bakedProfilesAsRecords(dellLaptopMachineTag);
	const std::string text = formatCameraListForDiagnostics(records);

	EXPECT_NE(text.find("Known cameras:"), std::string::npos);
	EXPECT_NE(text.find("HDMI USB Camera"), std::string::npos);
	EXPECT_NE(text.find("location=external"), std::string::npos);
	EXPECT_NE(text.find("Integrated_Webcam_HD"), std::string::npos);
	EXPECT_NE(text.find("location=front"), std::string::npos);
}

TEST(ResolveDeviceSelectorAgainstRecordsTest, BakedUsbHdmiCameraMatchesExampleSelector)
{
	const std::vector<CameraIdentityRecord> records =
		tests::bakedProfilesAsRecords(dellLaptopMachineTag);

	const CameraIdentityRecord resolved =
		resolveDeviceSelectorAgainstRecords(
			"model-substr:HDMI;location:EXTERNAL",
			records);

	EXPECT_EQ(
		resolved.id,
		"\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
	EXPECT_EQ(resolved.locationLabel, "external");
}

TEST(ResolveSelectorAgainstRecordsTest, BakedUsbHdmiCameraMatchesExampleSelector)
{
	const std::vector<CameraIdentityRecord> records =
		tests::bakedProfilesAsRecords(dellLaptopMachineTag);
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("model-substr:HDMI;location:EXTERNAL");

	const CameraIdentityRecord resolved =
		resolveSelectorAgainstRecords(criteria, records);

	EXPECT_EQ(
		resolved.id,
		"\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
	EXPECT_EQ(resolved.locationLabel, "external");
}

TEST(ResolveSelectorAgainstRecordsTest, BakedIntegratedWebcamMatchesExampleSelector)
{
	const std::vector<CameraIdentityRecord> records =
		tests::bakedProfilesAsRecords(dellLaptopMachineTag);
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("model-substr:Integrated;location:front");

	const CameraIdentityRecord resolved =
		resolveSelectorAgainstRecords(criteria, records);

	EXPECT_EQ(
		resolved.id,
		"\\_SB_.PCI0.XHC_.RHUB.HS04-4:1.0-1bcf:2b8a");
	EXPECT_EQ(resolved.locationLabel, "front");
}

TEST(ResolveSelectorAgainstRecordsTest, BakedUsbCameraMatchesLibcameraId)
{
	const std::vector<CameraIdentityRecord> records =
		tests::bakedProfilesAsRecords(dellLaptopMachineTag);
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector(
			"lcamera-id:\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");

	const CameraIdentityRecord resolved =
		resolveSelectorAgainstRecords(criteria, records);

	EXPECT_EQ(
		resolved.id,
		"\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
}

TEST(ResolveSelectorAgainstRecordsTest, MatchesLibcameraId)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("lcamera-id:/base/cam1");

	const CameraIdentityRecord resolved =
		resolveSelectorAgainstRecords(criteria, records);

	EXPECT_EQ(resolved.id, "/base/cam1");
}

TEST(ResolveSelectorAgainstRecordsTest, MatchesModelSubstrAndLocation)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("model-substr:Logitech;location:EXTERNAL");

	const CameraIdentityRecord resolved =
		resolveSelectorAgainstRecords(criteria, records);

	EXPECT_EQ(resolved.id, "/base/cam1");
}

TEST(ResolveSelectorAgainstRecordsTest, IndexSelectsNthCamera)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("index:2");

	const CameraIdentityRecord resolved =
		resolveSelectorAgainstRecords(criteria, records);

	EXPECT_EQ(resolved.id, "/base/cam2");
}

TEST(ResolveSelectorAgainstRecordsTest, IndexCombinedWithModel)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("index:0;model:imx219");

	const CameraIdentityRecord resolved =
		resolveSelectorAgainstRecords(criteria, records);

	EXPECT_EQ(resolved.id, "/base/cam0");
}

TEST(ResolveSelectorAgainstRecordsTest, ZeroMatchesThrowsWithDiagnostics)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("model:does-not-exist");

	try {
		resolveSelectorAgainstRecords(criteria, records);
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exc)
	{
		EXPECT_NE(
			std::string(exc.what()).find("No camera matches deviceSelector"),
			std::string::npos);
		EXPECT_NE(
			std::string(exc.what()).find("Known cameras:"),
			std::string::npos);
	}
}

TEST(ResolveSelectorAgainstRecordsTest, AmbiguousSelectorThrows)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("model:imx219");

	EXPECT_THROW(
		resolveSelectorAgainstRecords(criteria, records),
		std::runtime_error);
}

TEST(ResolveSelectorAgainstRecordsTest, IndexOutOfRangeThrows)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("index:9");

	EXPECT_THROW(
		resolveSelectorAgainstRecords(criteria, records),
		std::runtime_error);
}

TEST(ResolveSelectorAgainstRecordsTest, IndexConflictsWithOtherClauses)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("index:0;location:front");

	EXPECT_THROW(
		resolveSelectorAgainstRecords(criteria, records),
		std::runtime_error);
}

TEST(ResolveSelectorAgainstRecordsTest, InvalidIndexValueThrows)
{
	const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
	const std::vector<SelectorCriterion> criteria =
		parseDeviceSelector("index:not-a-number");

	EXPECT_THROW(
		resolveSelectorAgainstRecords(criteria, records),
		std::runtime_error);
}

} // namespace
} // namespace lcamera_dev
