#include <boost/asio/io_context.hpp>
#include <gtest/gtest.h>
#include <opts.h>
#include <spinscale/componentThread.h>
#include <spinscale/co/invokers.h>
#include <spinscale/syncCancelerForAsyncWork.h>
#include <support/exceptionAssertions.h>
#include <unistd.h>
#include <user/comparatorApiDesc.h>
#include <user/compute.h>
#include <user/stimulusBuffer.h>
#include <user/stimulusProducer.h>
#define CL_TARGET_OPENCL_VERSION 120
#include <CL/cl.h>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

namespace smo {
namespace stim_buff {
namespace {

class TestStimulusProducer
:	public StimulusProducer
{
public:
	using StimulusProducer::StimulusProducer;

	std::shared_ptr<StimulusBuffer> getOrCreateAttachedStimulusBuffer(
		const std::shared_ptr<device::DeviceAttachmentSpec>&
			deviceAttachmentSpec) override
	{
		(void)deviceAttachmentSpec;
		throw std::logic_error("not used in these unit tests");
	}

	bool exportsQualeIfaceApi(const std::string& qualeIfaceApi) const override
	{
		(void)qualeIfaceApi;
		return true;
	}

protected:
	sscl::co::ViralNonPostingInvoker<void> stimFrameProductionTimesliceCInd(
		sscl::SyncCancelerForAsyncWork& canceler) override
	{
		(void)canceler;
		co_return;
	}
};

std::optional<std::string> searchForLibInSmoSearchPathsStub(
	const std::string& libraryPath)
{
	(void)libraryPath;
	return std::nullopt;
}

std::shared_ptr<sscl::ComponentThread> componentThreadGetSelfStub()
{
	return sscl::ComponentThread::getSelf();
}

OptionParser& optionParserGetOptionsStub()
{
	return OptionParser::getOptions();
}

void releaseUseHostPtrBufferStub(
	std::shared_ptr<smo::compute::ClBuffer> buffer)
{
	(void)buffer;
}

std::shared_ptr<smo::compute::ComputeDevice> computeManagerGetDeviceStub()
{
	return nullptr;
}

void computeManagerReleaseDeviceStub(
	std::shared_ptr<smo::compute::ComputeDevice> device)
{
	(void)device;
}

std::shared_ptr<smo::cologex::ExportedComparatorTypeDesc>
comparatorManagerGetComparatorTypeStub(smo::cologex::ComparatorTypeId typeId)
{
	(void)typeId;
	return nullptr;
}

std::unique_ptr<smo::cologex::Comparator> comparatorGetNewInstanceStub(
	const std::shared_ptr<smo::cologex::ExportedComparatorTypeDesc>& comparatorType)
{
	(void)comparatorType;
	return nullptr;
}

std::shared_ptr<smo::compute::ClBuffer> createUseHostPtrBufferStub(
	void* hostPtr,
	size_t size,
	cl_mem_flags flags)
{
	static const std::vector<std::shared_ptr<smo::compute::ComputeDevice>>
		emptyDevices;
	return std::make_shared<smo::compute::ClBuffer>(
		hostPtr, size, flags, emptyDevices);
}

const SmoCallbacks kTestSmoCallbacks = {
	.searchForLibInSmoSearchPaths = searchForLibInSmoSearchPathsStub,
	.ComponentThread_getSelf = componentThreadGetSelfStub,
	.OptionParser_getOptions = optionParserGetOptionsStub,
	.ComputeManager_createUseHostPtrBuffer = createUseHostPtrBufferStub,
	.ComputeManager_releaseUseHostPtrBuffer = releaseUseHostPtrBufferStub,
	.ComputeManager_getDevice = computeManagerGetDeviceStub,
	.ComputeManager_releaseDevice = computeManagerReleaseDeviceStub,
	.ComparatorManager_getComparatorType =
		comparatorManagerGetComparatorTypeStub,
	.Comparator_getNewInstance = comparatorGetNewInstanceStub,
};

StagingBuffer::IOEngineConstraints testBufferConstraints()
{
	const size_t pageSize = static_cast<size_t>(sysconf(_SC_PAGE_SIZE));
	const size_t channelByteSize = pageSize;

	return StagingBuffer::IOEngineConstraints(
		1,
		channelByteSize,
		pageSize,
		pageSize);
}

std::shared_ptr<device::DeviceAttachmentSpec> makeAttachSpec(
	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 = "testBuff";
	spec->provider = "testProvider";
	spec->deviceSelector = deviceSelector;
	return spec;
}

std::shared_ptr<StimulusBuffer> makeAttachedTestBuffer(
	TestStimulusProducer& producer,
	const std::shared_ptr<device::DeviceAttachmentSpec>& attachSpec)
{
	const StagingBuffer::IOEngineConstraints constraints =
		testBufferConstraints();

	return std::make_shared<StimulusBuffer>(
		producer,
		attachSpec,
		CONFIG_STIMBUFF_FRAME_PERIOD_MS,
		constraints,
		constraints,
		kTestSmoCallbacks,
		CL_MEM_READ_WRITE);
}

class StimulusProducerQualeIfaceTest
:	public ::testing::Test
{
protected:
	boost::asio::io_context ioContext;
	std::shared_ptr<device::DeviceAttachmentSpec> producerSpec =
		makeAttachSpec("model-substr:USB;location:external", "colour-yuv-y");
	TestStimulusProducer producer{producerSpec, ioContext};
};

TEST_F(StimulusProducerQualeIfaceTest, HasBufferWithQualeIfaceApiDetectsChannel)
{
	EXPECT_FALSE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));

	const auto attachSpec = makeAttachSpec(
		"model-substr:USB;location:external", "colour-yuv-y", "cam-y");
	const auto buffer = makeAttachedTestBuffer(producer, attachSpec);
	ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(buffer));

	EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
	EXPECT_FALSE(producer.hasBufferWithQualeIfaceApi("colour-yuv-u"));
}

TEST_F(StimulusProducerQualeIfaceTest,
	AddAttachedStimulusBufferIfNotExistsAllowsSameQualeDifferentSelector)
{
	const auto firstSpec = makeAttachSpec(
		"model-substr:USB;location:external", "colour-yuv-y", "cam-y-0");
	const auto firstBuffer = makeAttachedTestBuffer(producer, firstSpec);
	ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(firstBuffer));

	const auto secondSpec = makeAttachSpec(
		"model-substr:USB Video Device;location:external",
		"colour-yuv-y",
		"cam-y-1");
	const auto secondBuffer = makeAttachedTestBuffer(producer, secondSpec);

	EXPECT_TRUE(producer.addAttachedStimulusBufferIfNotExists(secondBuffer));
	EXPECT_EQ(producer.attachedStimulusBuffers.size(), 2u);
	EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
	EXPECT_NE(firstSpec->deviceSelector, secondSpec->deviceSelector);
}

TEST_F(StimulusProducerQualeIfaceTest,
	EnsureNoDuplicateQualeIfaceRejectsAlternateSelector)
{
	const auto firstSpec = makeAttachSpec(
		"model-substr:USB;location:external", "colour-yuv-y", "cam-y-0");
	const auto firstBuffer = makeAttachedTestBuffer(producer, firstSpec);
	ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(firstBuffer));

	const auto duplicateQualeSpec = makeAttachSpec(
		"model-substr:USB Video Device;location:external",
		"colour-yuv-y",
		"cam-y-1");

	try {
		producer.ensureNoDuplicateQualeIface(
			duplicateQualeSpec->qualeIfaceApi);
		FAIL() << "Expected std::runtime_error";
	}
	catch (const std::runtime_error& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception,
			"duplicate qualeIface 'colour-yuv-y'");
	}
}

TEST_F(StimulusProducerQualeIfaceTest,
	EnsureNoDuplicateQualeIfaceAllowsDistinctChannels)
{
	const auto ySpec = makeAttachSpec(
		"model-substr:USB;location:external", "colour-yuv-y", "cam-y");
	ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(
		makeAttachedTestBuffer(producer, ySpec)));

	EXPECT_NO_THROW(
		producer.ensureNoDuplicateQualeIface("colour-yuv-u"));
	EXPECT_NO_THROW(
		producer.ensureNoDuplicateQualeIface("colour-yuv-v"));
}

TEST_F(StimulusProducerQualeIfaceTest,
	GetAttachedStimulusBufferByAttachIdentityIgnoresSelectorString)
{
	const auto attachedSpec = makeAttachSpec(
		"model-substr:USB;location:external", "colour-yuv-u", "cam-u");
	ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(
		makeAttachedTestBuffer(producer, attachedSpec)));

	const auto foundBuffer =
		producer.getAttachedStimulusBufferByAttachIdentity(
			"cam-u", "colour-yuv-u");
	ASSERT_NE(foundBuffer, nullptr);
	EXPECT_EQ(foundBuffer, producer.attachedStimulusBuffers.front());
	EXPECT_NE(
		foundBuffer->deviceAttachmentSpec->deviceSelector,
		"model-substr:USB Video Device;location:external");

	EXPECT_EQ(
		producer.getAttachedStimulusBufferByAttachIdentity(
			"cam-u", "colour-yuv-y"),
		nullptr);
	EXPECT_EQ(
		producer.getAttachedStimulusBufferByAttachIdentity(
			"cam-y", "colour-yuv-u"),
		nullptr);
}

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