#include <boost/asio/io_context.hpp>
#include <cameraIdentity.h>
#include <gtest/gtest.h>
#include <hilSmoCallbacksStub.h>
#include <lcameraBuffInternal.h>
#include <support/exceptionAssertions.h>
#include <yuvChannelStimulusBuffer.h>
#include <yuvStimProducer.h>
#include <memory>
#include <string>

namespace smo {
namespace stim_buff {
namespace lcamera_buff {
namespace {

constexpr const char *usbExternalSelector =
	"model-substr:USB;location:external";
constexpr const char *usbAliasSelector =
	"model-substr:USB Video Device;location:external";

std::shared_ptr<device::DeviceAttachmentSpec> makeLcameraAttachSpec(
	const std::string& deviceSelector,
	const std::string& qualeIfaceApi,
	const std::string& deviceIdentifier = "cam0")
{
	auto spec = std::make_shared<device::DeviceAttachmentSpec>();
	spec->deviceIdentifier = deviceIdentifier;
	spec->sensorType = 'e';
	spec->qualeIfaceApi = qualeIfaceApi;
	spec->stimBuffApi = "lcameraBuff";
	spec->provider = "lcameraDev";
	spec->deviceSelector = deviceSelector;
	spec->stimBuffApiParams = {
		{"v-res", "480p"},
		{"colour-space", "yuv"},
		{"opt-planar", ""},
	};
	return spec;
}

LcameraBuffParsedParams makeDefaultParsedParams()
{
	LcameraBuffParsedParams parsedParams;
	parsedParams.width = 640;
	parsedParams.height = 480;
	parsedParams.colourSpace = lcamera_dev::LcameraDevColourSpace::Yuv;
	parsedParams.fullPlanarIsOptional = true;
	return parsedParams;
}

lcamera_dev::LcameraDevConfiguredCameraMode makeYuyv480ConfiguredMode()
{
	lcamera_dev::LcameraDevConfiguredCameraMode configuredMode;
	configuredMode.width = 640;
	configuredMode.height = 480;
	configuredMode.colourSpace = lcamera_dev::LcameraDevColourSpace::Yuv;
	configuredMode.pixelFormatName = "YUYV";
	configuredMode.isFullyPlanar = false;
	configuredMode.planeCount = 1;
	return configuredMode;
}

lcamera_dev::CameraIdentityRecord makeTestCameraIdentity()
{
	lcamera_dev::CameraIdentityRecord identity;
	identity.id = "unit-test-camera-1";
	identity.model = "UnitTestCam";
	return identity;
}

class YuvStimProducerQualeIfaceTest
:	public ::testing::Test
{
protected:
	void SetUp() override
	{
		lcameraBuffSmoHooksPtr = &lcamera_buff_tests::hilSmoCallbacksStub();
	}

	void TearDown() override
	{
		lcameraBuffSmoHooksPtr = nullptr;
	}

	boost::asio::io_context ioContext;
	const LcameraBuffParsedParams parsedParams = makeDefaultParsedParams();
	const lcamera_dev::LcameraDevConfiguredCameraMode configuredMode =
		makeYuyv480ConfiguredMode();
	const lcamera_dev::CameraIdentityRecord cameraIdentity =
		makeTestCameraIdentity();

	YuvStimProducer makeProducer(
		const std::shared_ptr<device::DeviceAttachmentSpec>& producerSpec)
	{
		return YuvStimProducer(
			producerSpec,
			ioContext,
			nullptr,
			cameraIdentity,
			parsedParams,
			configuredMode);
	}
};

TEST_F(YuvStimProducerQualeIfaceTest, GetOrCreateSameSpecReturnsExistingBuffer)
{
	auto producerSpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-y");
	YuvStimProducer producer = makeProducer(producerSpec);

	const auto firstBuffer =
		producer.getOrCreateAttachedStimulusBuffer(producerSpec);
	ASSERT_NE(firstBuffer, nullptr);

	const auto secondBuffer =
		producer.getOrCreateAttachedStimulusBuffer(producerSpec);

	EXPECT_EQ(firstBuffer, secondBuffer);
	EXPECT_EQ(producer.attachedStimulusBuffers.size(), 1u);
}

TEST_F(YuvStimProducerQualeIfaceTest,
	RejectDuplicateQualeWhenAlternateSelectorResolvesToSameSession)
{
	/* One YuvStimProducer == one resolved libcamera camera id / session.
	 * A second DAP line may spell a different deviceSelector yet resolve to
	 * that same session via attachToExistingProducer; it must not attach the
	 * same qualeIface API twice.
	 */
	auto producerSpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-y");
	YuvStimProducer producer = makeProducer(producerSpec);

	const auto firstSpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-y", "cam-y-line0");
	ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(firstSpec), nullptr);

	const auto duplicateQualeAlternateSelectorSpec = makeLcameraAttachSpec(
		usbAliasSelector, "colour-yuv-y", "cam-y-line1");

	try {
		producer.getOrCreateAttachedStimulusBuffer(
			duplicateQualeAlternateSelectorSpec);
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception,
			"already attached for camera session");
		sscl::tests::expectExceptionMessageContains(
			exception,
			"colour-yuv-y");
	}

	EXPECT_EQ(producer.attachedStimulusBuffers.size(), 1u);
	EXPECT_NE(
		firstSpec->deviceSelector,
		duplicateQualeAlternateSelectorSpec->deviceSelector);
}

TEST_F(YuvStimProducerQualeIfaceTest,
	AllowDistinctQualeIfacesForAlternateSelectorsOnSameSession)
{
	auto producerSpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-y");
	YuvStimProducer producer = makeProducer(producerSpec);

	const auto ySpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-y", "cam-y");
	const auto uSpec = makeLcameraAttachSpec(
		usbAliasSelector, "colour-yuv-u", "cam-u");
	const auto vSpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-v", "cam-v");

	ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(ySpec), nullptr);
	ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(uSpec), nullptr);
	ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(vSpec), nullptr);

	EXPECT_EQ(producer.attachedStimulusBuffers.size(), 3u);
	EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
	EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-u"));
	EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-v"));
}

TEST_F(YuvStimProducerQualeIfaceTest,
	RejectDuplicateQualeAfterYViaSelectorAAndUViaSelectorB)
{
	auto producerSpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-y");
	YuvStimProducer producer = makeProducer(producerSpec);

	ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(
		makeLcameraAttachSpec(usbExternalSelector, "colour-yuv-y", "cam-y")),
		nullptr);
	ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(
		makeLcameraAttachSpec(usbAliasSelector, "colour-yuv-u", "cam-u")),
		nullptr);

	const auto duplicateYViaAlternateSelector = makeLcameraAttachSpec(
		usbAliasSelector, "colour-yuv-y", "cam-y-second-dap-line");

	EXPECT_THROW(
		producer.getOrCreateAttachedStimulusBuffer(
			duplicateYViaAlternateSelector),
		std::runtime_error);
	EXPECT_EQ(producer.attachedStimulusBuffers.size(), 2u);
}

TEST_F(YuvStimProducerQualeIfaceTest,
	FindAttachedBufferByAttachIdentityWithAlternateSelector)
{
	auto producerSpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-y");
	YuvStimProducer producer = makeProducer(producerSpec);

	const auto uSpec = makeLcameraAttachSpec(
		usbExternalSelector, "colour-yuv-u", "cam-u");
	ASSERT_NE(producer.getOrCreateAttachedStimulusBuffer(uSpec), nullptr);

	const auto foundBuffer =
		producer.getAttachedStimulusBufferByAttachIdentity(
			"cam-u", "colour-yuv-u");
	ASSERT_NE(foundBuffer, nullptr);
	EXPECT_EQ(
		foundBuffer->deviceAttachmentSpec->deviceSelector,
		usbExternalSelector);

	EXPECT_EQ(
		producer.getAttachedStimulusBuffer(
			makeLcameraAttachSpec(
				usbAliasSelector, "colour-yuv-u", "cam-u")),
		nullptr);
	EXPECT_NE(
		producer.getAttachedStimulusBufferByAttachIdentity(
			"cam-u", "colour-yuv-u"),
		nullptr);
}

} // namespace
} // namespace lcamera_buff
} // namespace stim_buff
} // namespace smo
