#include <boostAsioLinkageFix.h>

#include <catalogHelpers.h>
#include <cameraSession.h>
#include <lcameraDev.h>
#include <gtest/gtest.h>
#include <spinscale/co/nonViralTaskNursery.h>
#include <support/bakedDeviceCatalog.h>
#include <support/exceptionAssertions.h>
#include <support/probeComponentThread.h>
#include <cstdlib>
#include <functional>
#include <memory>
#include <string>
#include <vector>

namespace lcamera_dev {
namespace {

constexpr const char *hilEnvVar = "LCAMERADEV_HIL";
constexpr const char *machineEnvVar = "LCAMERADEV_MACHINE";
constexpr const char *configureWidthEnvVar = "LCAMERADEV_CONFIGURE_W";
constexpr const char *configureHeightEnvVar = "LCAMERADEV_CONFIGURE_H";
constexpr const char *defaultMachineTag = "dell-laptop";
constexpr unsigned defaultConfigureWidth = 640;
constexpr unsigned defaultConfigureHeight = 480;

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;
}

unsigned unsignedFromEnvironmentOrDefault(
	const char *envVar,
	unsigned defaultValue)
{
	const char *value = std::getenv(envVar);
	if (value == nullptr || std::string(value).empty()) {
		return defaultValue;
	}

	return static_cast<unsigned>(std::stoul(value));
}

LcameraDevCameraModeRequest configureRequestFromEnvironment()
{
	LcameraDevCameraModeRequest request;
	request.width = unsignedFromEnvironmentOrDefault(
		configureWidthEnvVar,
		defaultConfigureWidth);
	request.height = unsignedFromEnvironmentOrDefault(
		configureHeightEnvVar,
		defaultConfigureHeight);
	request.colourSpace = LcameraDevColourSpace::Yuv;
	request.fullPlanarIsOptional = false;
	return request;
}

sscl::co::NonViralNonPostingInvoker getOrCreateCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda,
	const char *deviceSelector,
	lcamera_dev::LcameraDevGetOrCreateResult& createResult)
{
	(void)exceptionStorage;
	(void)callerLambda;

	createResult = co_await lcameraDev_getOrCreateDeviceCReq(deviceSelector);
	co_return;
}

sscl::co::NonViralNonPostingInvoker configureCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda,
	const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
	const LcameraDevCameraModeRequest& request,
	LcameraDevConfiguredCameraMode& configuredMode)
{
	(void)callerLambda;

	configuredMode =
		co_await lcameraDev_configureSessionModeCReq(deviceSession, request);

	if (exceptionStorage) {
		std::rethrow_exception(exceptionStorage);
	}

	co_return;
}

sscl::co::NonViralNonPostingInvoker releaseCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda,
	const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession)
{
	(void)exceptionStorage;
	(void)callerLambda;

	co_await lcameraDev_releaseDeviceCReq(deviceSession);
	co_return;
}

void runLcameraDevMainAndNurseryTask(
	const std::function<void(
		const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
	sscl::tests::ProbeComponentThreadHarness harness("lcameraDev-configure-hil");
	harness.runSync(
		[&work](const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			lcameraDev_main(componentThread);
			work(componentThread);
			lcameraDev_exit();
		});
}

void runNonViralNurseryRethrowingOnComponentThread(
	const std::shared_ptr<sscl::ComponentThread>& componentThread,
	const std::function<sscl::co::NonViralNonPostingInvoker(
		sscl::co::NonViralTaskNursery::Slot::Lease&)>& invokerFactory)
{
	std::exception_ptr slotException;

	sscl::co::NonViralTaskNursery nursery;
	nursery.openAdmission();
	nursery.launch(
		invokerFactory,
		[&slotException](std::exception_ptr& exceptionPtr)
		{
			slotException = exceptionPtr;
		});
	nursery.closeAdmission();
	nursery.syncAwaitAllSettlements(componentThread->getIoContext());

	if (slotException) {
		std::rethrow_exception(slotException);
	}
}

bool configureSessionOrExpectPlanarFailure(
	const std::shared_ptr<sscl::ComponentThread>& componentThread,
	const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
	const LcameraDevCameraModeRequest& request,
	LcameraDevConfiguredCameraMode& configuredMode,
	const char *profileTag)
{
	try {
		runNonViralNurseryRethrowingOnComponentThread(
			componentThread,
			[&deviceSession, &request, &configuredMode](
				sscl::co::NonViralTaskNursery::Slot::Lease& lease)
			{
				return configureCInd(
					lease.getExceptionStorage(),
					lease.getCallerLambda(),
					deviceSession,
					request,
					configuredMode);
			});

		EXPECT_TRUE(configuredMode.isFullyPlanar) << profileTag;
		EXPECT_GE(configuredMode.width, 1u) << profileTag;
		EXPECT_GE(configuredMode.height, 1u) << profileTag;
		return true;
	}
	catch (const std::exception& exception)
	{
		sscl::tests::expectExceptionMessageContains(
			exception,
			"planar");
		return false;
	}
}

void configureProfileExpectingPlanarOrExplicitFailure(
	const test_fixtures::BakedCameraProfile *profile,
	const LcameraDevCameraModeRequest& request,
	const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
	lcamera_dev::LcameraDevGetOrCreateResult createResult;

	sscl::tests::runNonViralNurseryOnComponentThread(
		componentThread,
		[profile, &createResult](
			sscl::co::NonViralTaskNursery::Slot::Lease& lease)
		{
			return getOrCreateCInd(
				lease.getExceptionStorage(),
				lease.getCallerLambda(),
				profile->exampleSelector,
				createResult);
		});

	EXPECT_TRUE(createResult.deviceSession != nullptr)
		<< profile->profileTag;

	LcameraDevConfiguredCameraMode configuredMode;
	configureSessionOrExpectPlanarFailure(
		componentThread,
		createResult.deviceSession,
		request,
		configuredMode,
		profile->profileTag);

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

class LcameraDevConfigureHilTest : 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());
		configureRequest = configureRequestFromEnvironment();

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

	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;
	LcameraDevCameraModeRequest configureRequest;
};

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

	runLcameraDevMainAndNurseryTask(
		[this, profile](
			const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			configureProfileExpectingPlanarOrExplicitFailure(
				profile,
				configureRequest,
				componentThread);
		});
}

TEST_F(LcameraDevConfigureHilTest, ConfigureIntegratedWebcamYuv)
{
	const test_fixtures::BakedCameraProfile *profile =
		findProfile("integrated_webcam");
	if (!profile) {
		GTEST_SKIP() << "integrated_webcam profile not available";
	}

	runLcameraDevMainAndNurseryTask(
		[this, profile](
			const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			configureProfileExpectingPlanarOrExplicitFailure(
				profile,
				configureRequest,
				componentThread);
		});
}

TEST_F(LcameraDevConfigureHilTest, ConfiguredModeMatchesRequestDimensions)
{
	const test_fixtures::BakedCameraProfile *profile =
		findProfile("integrated_webcam");
	if (!profile) {
		GTEST_SKIP() << "integrated_webcam profile not available";
	}

	bool skipForMissingPlanarYuv = false;

	runLcameraDevMainAndNurseryTask(
		[this, profile, &skipForMissingPlanarYuv](
			const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			lcamera_dev::LcameraDevGetOrCreateResult createResult;

			sscl::tests::runNonViralNurseryOnComponentThread(
				componentThread,
				[profile, &createResult](
					sscl::co::NonViralTaskNursery::Slot::Lease& lease)
				{
					return getOrCreateCInd(
						lease.getExceptionStorage(),
						lease.getCallerLambda(),
						profile->exampleSelector,
						createResult);
				});

			LcameraDevConfiguredCameraMode configuredMode;
			const bool configureSucceeded =
				configureSessionOrExpectPlanarFailure(
					componentThread,
					createResult.deviceSession,
					configureRequest,
					configuredMode,
					profile->profileTag);

			if (!configureSucceeded) {
				skipForMissingPlanarYuv = true;
			}
			else
			{
				EXPECT_GE(configuredMode.width, 1u);
				EXPECT_GE(configuredMode.height, 1u);
				EXPECT_LE(
					configuredMode.width,
					configureRequest.width);
				EXPECT_LE(
					configuredMode.height,
					configureRequest.height);
			}

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

	if (skipForMissingPlanarYuv) {
		GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
	}
}

TEST_F(LcameraDevConfigureHilTest, IdenticalReconfigureIsNoOp)
{
	const test_fixtures::BakedCameraProfile *profile =
		findProfile("integrated_webcam");
	if (!profile) {
		GTEST_SKIP() << "integrated_webcam profile not available";
	}

	bool skipForMissingPlanarYuv = false;

	runLcameraDevMainAndNurseryTask(
		[this, profile, &skipForMissingPlanarYuv](
			const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			lcamera_dev::LcameraDevGetOrCreateResult createResult;

			sscl::tests::runNonViralNurseryOnComponentThread(
				componentThread,
				[profile, &createResult](
					sscl::co::NonViralTaskNursery::Slot::Lease& lease)
				{
					return getOrCreateCInd(
						lease.getExceptionStorage(),
						lease.getCallerLambda(),
						profile->exampleSelector,
						createResult);
				});

			LcameraDevConfiguredCameraMode firstMode;
			LcameraDevConfiguredCameraMode secondMode;

			const bool firstConfigureSucceeded =
				configureSessionOrExpectPlanarFailure(
					componentThread,
					createResult.deviceSession,
					configureRequest,
					firstMode,
					profile->profileTag);

			if (!firstConfigureSucceeded) {
				skipForMissingPlanarYuv = true;
			}
			else
			{
				const int configureCallCountAfterFirst =
					createResult.deviceSession
						->getLibcameraConfigureCallCount();

				runNonViralNurseryRethrowingOnComponentThread(
					componentThread,
					[&createResult, &secondMode, this](
						sscl::co::NonViralTaskNursery::Slot::Lease& lease)
					{
						return configureCInd(
							lease.getExceptionStorage(),
							lease.getCallerLambda(),
							createResult.deviceSession,
							configureRequest,
							secondMode);
					});

				EXPECT_EQ(
					createResult.deviceSession
						->getLibcameraConfigureCallCount(),
					configureCallCountAfterFirst);
				EXPECT_EQ(secondMode.pixelFormatName, firstMode.pixelFormatName);
				EXPECT_EQ(secondMode.width, firstMode.width);
				EXPECT_EQ(secondMode.height, firstMode.height);
			}

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

	if (skipForMissingPlanarYuv) {
		GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
	}
}

TEST_F(LcameraDevConfigureHilTest, ConflictingReconfigureThrows)
{
	const test_fixtures::BakedCameraProfile *profile =
		findProfile("integrated_webcam");
	if (!profile) {
		GTEST_SKIP() << "integrated_webcam profile not available";
	}

	bool skipForMissingPlanarYuv = false;

	runLcameraDevMainAndNurseryTask(
		[this, profile, &skipForMissingPlanarYuv](
			const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			lcamera_dev::LcameraDevGetOrCreateResult createResult;

			sscl::tests::runNonViralNurseryOnComponentThread(
				componentThread,
				[profile, &createResult](
					sscl::co::NonViralTaskNursery::Slot::Lease& lease)
				{
					return getOrCreateCInd(
						lease.getExceptionStorage(),
						lease.getCallerLambda(),
						profile->exampleSelector,
						createResult);
				});

			LcameraDevConfiguredCameraMode configuredMode;
			const bool configureSucceeded =
				configureSessionOrExpectPlanarFailure(
					componentThread,
					createResult.deviceSession,
					configureRequest,
					configuredMode,
					profile->profileTag);

			if (!configureSucceeded) {
				skipForMissingPlanarYuv = true;
			}
			else
			{
				LcameraDevCameraModeRequest conflictingRequest =
					configureRequest;
				conflictingRequest.width = configureRequest.width + 64;

				EXPECT_THROW(
					runNonViralNurseryRethrowingOnComponentThread(
						componentThread,
						[&createResult, &conflictingRequest](
							sscl::co::NonViralTaskNursery::Slot::Lease& lease)
						{
							LcameraDevConfiguredCameraMode ignoredMode;
							return configureCInd(
								lease.getExceptionStorage(),
								lease.getCallerLambda(),
								createResult.deviceSession,
								conflictingRequest,
								ignoredMode);
						}),
					std::runtime_error);
			}

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

	if (skipForMissingPlanarYuv) {
		GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
	}
}

void configureProfileWithOptPlanarExpectingYuyv(
	const test_fixtures::BakedCameraProfile *profile,
	const LcameraDevCameraModeRequest& request,
	const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
	lcamera_dev::LcameraDevGetOrCreateResult createResult;

	sscl::tests::runNonViralNurseryOnComponentThread(
		componentThread,
		[profile, &createResult](
			sscl::co::NonViralTaskNursery::Slot::Lease& lease)
		{
			return getOrCreateCInd(
				lease.getExceptionStorage(),
				lease.getCallerLambda(),
				profile->exampleSelector,
				createResult);
		});

	EXPECT_TRUE(createResult.deviceSession != nullptr)
		<< profile->profileTag;

	LcameraDevConfiguredCameraMode configuredMode;
	LcameraDevCameraModeRequest optPlanarRequest = request;
	optPlanarRequest.fullPlanarIsOptional = true;

	runNonViralNurseryRethrowingOnComponentThread(
		componentThread,
		[&createResult, &optPlanarRequest, &configuredMode](
			sscl::co::NonViralTaskNursery::Slot::Lease& lease)
		{
			return configureCInd(
				lease.getExceptionStorage(),
				lease.getCallerLambda(),
				createResult.deviceSession,
				optPlanarRequest,
				configuredMode);
		});

	EXPECT_FALSE(configuredMode.isFullyPlanar) << profile->profileTag;
	EXPECT_EQ(configuredMode.planeCount, 1u) << profile->profileTag;
	EXPECT_EQ(configuredMode.pixelFormatName, "YUYV") << profile->profileTag;
	EXPECT_GE(configuredMode.width, 1u) << profile->profileTag;
	EXPECT_GE(configuredMode.height, 1u) << profile->profileTag;

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

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

	runLcameraDevMainAndNurseryTask(
		[this, profile](
			const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			configureProfileWithOptPlanarExpectingYuyv(
				profile,
				configureRequest,
				componentThread);
		});
}

TEST_F(LcameraDevConfigureHilTest, ConfigureIntegratedWebcamYuvWithOptPlanarSelectsYuyv)
{
	const test_fixtures::BakedCameraProfile *profile =
		findProfile("integrated_webcam");
	if (!profile) {
		GTEST_SKIP() << "integrated_webcam profile not available";
	}

	runLcameraDevMainAndNurseryTask(
		[this, profile](
			const std::shared_ptr<sscl::ComponentThread>& componentThread)
		{
			configureProfileWithOptPlanarExpectingYuyv(
				profile,
				configureRequest,
				componentThread);
		});
}

} // namespace
} // namespace lcamera_dev
