#include <boostAsioLinkageFix.h>

#include <gtest/gtest.h>
#include <hilSmoCallbacksStub.h>
#include <lcameraBuffInternal.h>
#include <lcameraBuffParams.h>
#include <support/bakedDeviceCatalog.h>
#include <support/probeComponentThread.h>
#include <user/deviceAttachmentSpec.h>
#include <user/senseApiDesc.h>
#include <yuvCaptureLayout.h>
#include <pixelAndColorFormatDecisions.h>
#include <yuvChannelStimulusBuffer.h>
#include <yuvStimProducer.h>
#include <spinscale/co/nonViralTaskNursery.h>
#include <cstdlib>
#include <memory>
#include <string>
#include <vector>

namespace lcamera_buff_tests {
namespace {

constexpr const char *hilEnvVar = "LCAMERADEV_HIL";
constexpr const char *machineEnvVar = "LCAMERADEV_MACHINE";
constexpr const char *defaultMachineTag = "dell-laptop";

bool hilTestsEnabled()
{
	const char *value = std::getenv(hilEnvVar);
	return value != nullptr && std::string(value) == "1";
}

std::string machineTagFromEnvironment()
{
	const char *value = std::getenv(machineEnvVar);
	if (value != nullptr && std::string(value).size() > 0) {
		return value;
	}

	return defaultMachineTag;
}

std::shared_ptr<smo::device::DeviceAttachmentSpec> makeAttachSpec(
	const test_fixtures::BakedCameraProfile *profile,
	const char *qualeIfaceApi)
{
	auto spec = std::make_shared<smo::device::DeviceAttachmentSpec>();
	spec->deviceIdentifier = "cam0";
	spec->sensorType = 'e';
	spec->qualeIfaceApi = qualeIfaceApi;
	spec->stimBuffApi = "lcameraBuff";
	spec->provider = "lcameraDev";
	spec->deviceSelector = profile->exampleSelector;
	spec->stimBuffApiParams = {
		{"v-res", "480p"},
		{"colour-space", "yuv"},
		{"opt-planar", ""},
	};
	return spec;
}

std::shared_ptr<smo::device::DeviceAttachmentSpec> makeDetachSpec(
	const test_fixtures::BakedCameraProfile *profile,
	const char *qualeIfaceApi,
	const char *deviceSelectorOverride)
{
	auto spec = makeAttachSpec(profile, qualeIfaceApi);
	spec->deviceSelector = deviceSelectorOverride;
	return spec;
}

sscl::co::NonViralNonPostingInvoker initializeBuffCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda)
{
	(void)exceptionStorage;
	(void)callerLambda;

	co_await smo::stim_buff::lcamera_buff::lcameraBuff_initializeCInd();
	co_return;
}

sscl::co::NonViralNonPostingInvoker finalizeBuffCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda)
{
	(void)exceptionStorage;
	(void)callerLambda;

	co_await smo::stim_buff::lcamera_buff::lcameraBuff_finalizeCInd();
	co_return;
}

sscl::co::NonViralNonPostingInvoker attachDeviceCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda,
	const std::shared_ptr<smo::device::DeviceAttachmentSpec>& spec,
	const std::shared_ptr<sscl::ComponentThread>& componentThread,
	smo::stim_buff::StimBuffDeviceOpResult& result)
{
	(void)exceptionStorage;
	(void)callerLambda;

	result = co_await smo::stim_buff::lcamera_buff::lcameraBuff_attachDeviceCReq(
		sscl::co::ExplicitPostTarget(componentThread->getIoContext()),
		spec,
		componentThread);

	co_return;
}

sscl::co::NonViralNonPostingInvoker detachDeviceCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda,
	const std::shared_ptr<smo::device::DeviceAttachmentSpec>& spec,
	smo::stim_buff::StimBuffDeviceOpResult& result)
{
	(void)exceptionStorage;
	(void)callerLambda;

	result = co_await smo::stim_buff::lcamera_buff::lcameraBuff_detachDeviceCReq(
		sscl::co::ExplicitPostTarget(
			smo::stim_buff::lcamera_buff::lcameraBuffThreadingModelDesc
				.componentThread->getIoContext()),
		spec);

	co_return;
}

void runOnProbeThread(
	const std::function<void(
		const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
	sscl::tests::ProbeComponentThreadHarness harness("lcameraBuff-configure-hil");
	harness.runSync(work);
}

class LcameraBuffConfigureHilTest : public ::testing::Test
{
protected:
	void SetUp() override
	{
		if (!hilTestsEnabled()) {
			GTEST_SKIP() << "Set " << hilEnvVar << "=1 to run hardware tests";
		}

		machineTag = machineTagFromEnvironment();
		requiredProfiles =
			sscl::tests::requiredProfilesForMachine(machineTag.c_str());

		if (requiredProfiles.empty()) {
			GTEST_SKIP() << "No baked profiles for machine tag "
				<< machineTag;
		}

		const test_fixtures::BakedCameraProfile *profile =
			findProfile("usb_hdmi_camera");
		if (!profile) {
			GTEST_SKIP() << "usb_hdmi_camera profile not available";
		}

		usbProfile = profile;
	}

	const test_fixtures::BakedCameraProfile *findProfile(
		const char *profileTag) const
	{
		for (const test_fixtures::BakedCameraProfile *profile :
			requiredProfiles)
		{
			if (std::string(profile->profileTag) == profileTag) {
				return profile;
			}
		}

		return nullptr;
	}

	std::string machineTag;
	std::vector<const test_fixtures::BakedCameraProfile *> requiredProfiles;
	const test_fixtures::BakedCameraProfile *usbProfile = nullptr;
};

TEST_F(LcameraBuffConfigureHilTest, AttachThreeYuvChannelsWithOptPlanar480p)
{
	const char *libPathEnv = std::getenv("LCAMERADEV_LIB_PATH");
	if (libPathEnv == nullptr || std::string(libPathEnv).empty())
	{
		GTEST_SKIP() << "Set LCAMERADEV_LIB_PATH to liblcameraDev.so.0 for HIL attach";
	}

	runOnProbeThread(
		[this](const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			smo::stim_buff::lcamera_buff::lcameraBuffSmoHooksPtr =
				&hilSmoCallbacksStub();
			smo::stim_buff::lcamera_buff::lcameraBuffThreadingModelDesc
				.componentThread = componentThread;

			sscl::tests::runNonViralNurseryOnComponentThread(
				componentThread,
				[](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
				{
					return initializeBuffCInd(
						lease.getExceptionStorage(),
						lease.getCallerLambda());
				});

			std::shared_ptr<smo::stim_buff::lcamera_buff::YuvStimProducer>
				producer;
			size_t expectedBufferCount = 0;

			for (const char *qualeIfaceApi :
				{"colour-yuv-y", "colour-yuv-u", "colour-yuv-v"})
			{
				const std::shared_ptr<smo::device::DeviceAttachmentSpec> spec =
					makeAttachSpec(usbProfile, qualeIfaceApi);

				smo::stim_buff::StimBuffDeviceOpResult attachResult;
				sscl::tests::runNonViralNurseryOnComponentThread(
					componentThread,
					[&spec, &componentThread, &attachResult](
						sscl::co::NonViralTaskNursery::Slot::Lease& lease)
					{
						return attachDeviceCInd(
							lease.getExceptionStorage(),
							lease.getCallerLambda(),
							spec,
							componentThread,
							attachResult);
					});

				ASSERT_TRUE(attachResult.success);
				++expectedBufferCount;

				ASSERT_EQ(
					smo::stim_buff::lcamera_buff::managedStimulusProducers.size(),
					1u);
				producer = smo::stim_buff::lcamera_buff
					::managedStimulusProducers.front();
				ASSERT_TRUE(producer != nullptr);
				EXPECT_EQ(
					producer->attachedStimulusBuffers.size(),
					expectedBufferCount);
			}

			ASSERT_TRUE(producer != nullptr);
			EXPECT_EQ(
				producer->configuredMode.pixelFormatName,
				"YUYV");
			EXPECT_EQ(
				producer->layoutPath,
				smo::stim_buff::lcamera_buff::YuvCaptureLayoutPath
					::PackedDeinterleave);
			EXPECT_EQ(
				producer->chromaSubsampling,
				smo::stim_buff::lcamera_buff::YuvChromaSubsampling::Yuv422);

			const size_t expectedUBytes =
				smo::stim_buff::lcamera_buff
					::computeDeinterleavedChannelByteSize(
						smo::stim_buff::lcamera_buff::YuvChannelKind::U,
						producer->configuredMode.width,
						producer->configuredMode.height,
						smo::stim_buff::lcamera_buff
							::YuvChromaSubsampling::Yuv422);
			EXPECT_EQ(expectedUBytes, 320u * 480u);

			for (const std::shared_ptr<smo::stim_buff::StimulusBuffer>& bufferBase :
				producer->attachedStimulusBuffers)
			{
				auto channelBuffer =
					std::dynamic_pointer_cast<
						smo::stim_buff::lcamera_buff::YuvChannelStimulusBuffer>(
							bufferBase);
				ASSERT_TRUE(channelBuffer != nullptr);

				if (channelBuffer->channelKind
					== smo::stim_buff::lcamera_buff::YuvChannelKind::U
					|| channelBuffer->channelKind
						== smo::stim_buff::lcamera_buff::YuvChannelKind::V)
				{
					EXPECT_EQ(channelBuffer->channelByteSize, expectedUBytes);
				}
			}

			for (const char *qualeIfaceApi :
				{"colour-yuv-v", "colour-yuv-u", "colour-yuv-y"})
			{
				const std::shared_ptr<smo::device::DeviceAttachmentSpec> spec =
					(std::string(qualeIfaceApi) == "colour-yuv-u")
						? makeDetachSpec(
							usbProfile,
							qualeIfaceApi,
							"model-substr:HDMI USB Camera;location:external")
						: makeAttachSpec(usbProfile, qualeIfaceApi);

				smo::stim_buff::StimBuffDeviceOpResult detachResult;
				sscl::tests::runNonViralNurseryOnComponentThread(
					componentThread,
					[&spec, &detachResult](
						sscl::co::NonViralTaskNursery::Slot::Lease& lease)
					{
						return detachDeviceCInd(
							lease.getExceptionStorage(),
							lease.getCallerLambda(),
							spec,
							detachResult);
					});

				ASSERT_TRUE(detachResult.success);
			}

			sscl::tests::runNonViralNurseryOnComponentThread(
				componentThread,
				[](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
				{
					return finalizeBuffCInd(
						lease.getExceptionStorage(),
						lease.getCallerLambda());
				});
		});
}

} // namespace
} // namespace lcamera_buff_tests
