#include "ShaderPackageRegistry.h"

#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <string>
#include <vector>

namespace
{
int gFailures = 0;

void Expect(bool condition, const char* message)
{
	if (condition)
		return;

	std::cerr << "FAIL: " << message << "\n";
	++gFailures;
}

std::filesystem::path MakeTestRoot()
{
	const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
	std::filesystem::path root = std::filesystem::temp_directory_path() / ("video-shader-registry-tests-" + std::to_string(stamp));
	std::filesystem::create_directories(root);
	return root;
}

void WriteFile(const std::filesystem::path& path, const std::string& contents)
{
	std::filesystem::create_directories(path.parent_path());
	std::ofstream output(path, std::ios::binary);
	output << contents;
}

void WriteShaderPackage(const std::filesystem::path& root, const std::string& directoryName, const std::string& manifest)
{
	const std::filesystem::path packageRoot = root / directoryName;
	WriteFile(packageRoot / "shader.json", manifest);
	WriteFile(packageRoot / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
}

void TestValidManifest()
{
	const std::filesystem::path root = MakeTestRoot();
	WriteFile(root / "look" / "mask.png", "not a real png, but enough for existence checks");
	WriteFile(root / "look" / "Inter.ttf", "not a real font, but enough for existence checks");
	WriteShaderPackage(root, "look", R"({
		"id": "look-01",
		"name": "Look 01",
		"description": "Test package",
		"category": "Tests",
		"entryPoint": "shadeVideo",
		"textures": [{ "id": "maskTex", "path": "mask.png" }],
		"fonts": [{ "id": "inter", "path": "Inter.ttf" }],
		"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
		"feedback": { "enabled": true },
		"parameters": [
			{ "id": "gain", "label": "Gain", "description": "Scales the output intensity.", "type": "float", "default": 0.5, "min": 0, "max": 1 },
			{ "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 },
			{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
				{ "value": "soft", "label": "Soft" },
				{ "value": "hard", "label": "Hard" }
			] },
			{ "id": "flash", "label": "Flash", "type": "trigger" }
		]
	})");

	ShaderPackageRegistry registry(4);
	ShaderPackage package;
	std::string error;
	Expect(registry.ParseManifest(root / "look" / "shader.json", package, error), "valid manifest parses");
	Expect(package.id == "look-01", "manifest id is preserved");
	Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse");
	Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse");
	Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
	Expect(package.feedback.enabled && package.feedback.writePassId == "main", "feedback defaults to the implicit main pass");
	Expect(package.parameters.size() == 4, "parameters parse");
	Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse");
	Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
	Expect(package.parameters[3].type == ShaderParameterType::Trigger, "trigger parameter parses");
	Expect(package.passes.size() == 1 && package.passes[0].id == "main", "legacy manifests get an implicit main pass");

	std::filesystem::remove_all(root);
}

void TestExplicitPassManifest()
{
	const std::filesystem::path root = MakeTestRoot();
	WriteShaderPackage(root, "multi", R"({
		"id": "multi-pass",
		"name": "Multi Pass",
		"passes": [
			{ "id": "blurX", "source": "blur-x.slang", "entryPoint": "blurHorizontal", "inputs": ["layerInput"], "output": "blurredX" },
			{ "id": "final", "source": "final.slang", "entryPoint": "finish", "inputs": ["blurredX"], "output": "layerOutput" }
		],
		"feedback": { "enabled": true, "writePass": "blurX" },
		"parameters": []
	})");
	WriteFile(root / "multi" / "blur-x.slang", "float4 blurHorizontal(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
	WriteFile(root / "multi" / "final.slang", "float4 finish(float2 uv) { return float4(uv, 1.0, 1.0); }\n");

	ShaderPackageRegistry registry(4);
	ShaderPackage package;
	std::string error;
	Expect(registry.ParseManifest(root / "multi" / "shader.json", package, error), "explicit pass manifest parses");
	Expect(package.passes.size() == 2, "explicit passes parse");
	Expect(package.passes[0].id == "blurX" && package.passes[0].entryPoint == "blurHorizontal", "first pass metadata parses");
	Expect(package.passes[0].inputNames.size() == 1 && package.passes[0].inputNames[0] == "layerInput", "pass inputs parse");
	Expect(package.passes[1].outputName == "layerOutput", "pass output parses");
	Expect(package.feedback.enabled && package.feedback.writePassId == "blurX", "explicit feedback write pass parses");

	std::filesystem::remove_all(root);
}

void TestMissingFontAsset()
{
	const std::filesystem::path root = MakeTestRoot();
	WriteShaderPackage(root, "bad-font", R"({
		"id": "bad-font",
		"name": "Bad Font",
		"fonts": [{ "id": "missingFont", "path": "missing.ttf" }],
		"parameters": []
	})");

	ShaderPackageRegistry registry(4);
	ShaderPackage package;
	std::string error;
	Expect(!registry.ParseManifest(root / "bad-font" / "shader.json", package, error), "missing font asset is rejected");
	Expect(error.find("font asset not found") != std::string::npos, "missing font error is clear");

	std::filesystem::remove_all(root);
}

void TestInvalidManifest()
{
	const std::filesystem::path root = MakeTestRoot();
	WriteShaderPackage(root, "bad", R"({
		"id": "bad",
		"name": "Bad",
		"entryPoint": "not-valid!",
		"parameters": []
	})");

	ShaderPackageRegistry registry(4);
	ShaderPackage package;
	std::string error;
	Expect(!registry.ParseManifest(root / "bad" / "shader.json", package, error), "invalid shader identifier is rejected");
	Expect(error.find("entryPoint") != std::string::npos, "invalid manifest error names the bad field");

	std::filesystem::remove_all(root);
}

void TestInvalidTemporalSettings()
{
	struct Case
	{
		const char* directoryName;
		const char* temporalJson;
		const char* expectedError;
	};

	const Case cases[] = {
		{ "bad-source", R"({ "enabled": true, "historySource": "previousOutput", "historyLength": 2 })", "Unsupported temporal historySource" },
		{ "missing-source", R"({ "enabled": true, "historyLength": 2 })", "historySource" },
		{ "missing-length", R"({ "enabled": true, "historySource": "source" })", "historyLength" },
		{ "string-length", R"({ "enabled": true, "historySource": "source", "historyLength": "2" })", "historyLength" },
		{ "zero-length", R"({ "enabled": true, "historySource": "source", "historyLength": 0 })", "positive integer" },
		{ "negative-length", R"({ "enabled": true, "historySource": "source", "historyLength": -1 })", "positive integer" },
		{ "fractional-length", R"({ "enabled": true, "historySource": "source", "historyLength": 1.5 })", "positive integer" },
	};

	const std::filesystem::path root = MakeTestRoot();
	for (const Case& testCase : cases)
	{
		WriteShaderPackage(root, testCase.directoryName, std::string(R"({
			"id": ")") + testCase.directoryName + R"(",
			"name": "Bad Temporal",
			"temporal": )" + testCase.temporalJson + R"(,
			"parameters": []
		})");

		ShaderPackageRegistry registry(4);
		ShaderPackage package;
		std::string error;
		Expect(!registry.ParseManifest(root / testCase.directoryName / "shader.json", package, error), "invalid temporal manifest is rejected");
		Expect(error.find(testCase.expectedError) != std::string::npos, "invalid temporal error explains the rejected field");
	}

	std::filesystem::remove_all(root);
}

void TestInvalidFeedbackSettings()
{
	const std::filesystem::path root = MakeTestRoot();
	WriteShaderPackage(root, "bad-feedback", R"({
		"id": "bad-feedback",
		"name": "Bad Feedback",
		"feedback": { "enabled": true, "writePass": "missingPass" },
		"parameters": []
	})");

	ShaderPackageRegistry registry(4);
	ShaderPackage package;
	std::string error;
	Expect(!registry.ParseManifest(root / "bad-feedback" / "shader.json", package, error), "invalid feedback manifest is rejected");
	Expect(error.find("writePass") != std::string::npos, "invalid feedback error names writePass");

	std::filesystem::remove_all(root);
}

void TestDisabledTemporalSettingsAreIgnored()
{
	const std::filesystem::path root = MakeTestRoot();
	WriteShaderPackage(root, "disabled-temporal", R"({
		"id": "disabled-temporal",
		"name": "Disabled Temporal",
		"temporal": { "enabled": false, "historySource": "not-supported", "historyLength": 0 },
		"parameters": []
	})");

	ShaderPackageRegistry registry(4);
	ShaderPackage package;
	std::string error;
	Expect(registry.ParseManifest(root / "disabled-temporal" / "shader.json", package, error), "disabled temporal settings are ignored");
	Expect(!package.temporal.enabled, "disabled temporal package stays non-temporal");
	Expect(package.temporal.effectiveHistoryLength == 0, "disabled temporal package has no effective history");

	std::filesystem::remove_all(root);
}

void TestDuplicateScan()
{
	const std::filesystem::path root = MakeTestRoot();
	WriteShaderPackage(root, "a", R"({ "id": "dupe", "name": "A", "parameters": [] })");
	WriteShaderPackage(root, "b", R"({ "id": "dupe", "name": "B", "parameters": [] })");

	ShaderPackageRegistry registry(4);
	std::map<std::string, ShaderPackage> packages;
	std::vector<std::string> order;
	std::vector<ShaderPackageStatus> statuses;
	std::string error;
	Expect(registry.Scan(root, packages, order, statuses, error), "duplicate package ids do not fail the whole scan");
	Expect(packages.size() == 1, "first duplicate package remains available");
	Expect(statuses.size() == 2, "duplicate package is surfaced in shader status list");
	Expect(std::any_of(statuses.begin(), statuses.end(), [](const ShaderPackageStatus& status) {
		return !status.available && status.error.find("Duplicate shader id") != std::string::npos;
	}), "duplicate scan error is shown on unavailable package");

	std::filesystem::remove_all(root);
}

void TestInvalidPackageDoesNotFailScan()
{
	const std::filesystem::path root = MakeTestRoot();
	WriteShaderPackage(root, "good", R"({ "id": "good", "name": "Good", "parameters": [] })");
	WriteShaderPackage(root, "bad", R"({
		"id": "bad",
		"name": "Bad",
		"parameters": [{ "id": "enabled", "label": "Enabled", "type": "boolean" }]
	})");

	ShaderPackageRegistry registry(4);
	std::map<std::string, ShaderPackage> packages;
	std::vector<std::string> order;
	std::vector<ShaderPackageStatus> statuses;
	std::string error;
	Expect(registry.Scan(root, packages, order, statuses, error), "invalid package does not fail the whole scan");
	Expect(packages.find("good") != packages.end(), "valid package remains available");
	Expect(packages.find("bad") == packages.end(), "invalid package is not available for rendering");
	Expect(std::any_of(statuses.begin(), statuses.end(), [](const ShaderPackageStatus& status) {
		return status.id.find("bad") != std::string::npos && !status.available && status.error.find("Unsupported parameter type") != std::string::npos;
	}), "invalid package is surfaced with its parse error");

	std::filesystem::remove_all(root);
}
}

int main()
{
	TestValidManifest();
	TestExplicitPassManifest();
	TestMissingFontAsset();
	TestInvalidManifest();
	TestInvalidTemporalSettings();
	TestInvalidFeedbackSettings();
	TestDisabledTemporalSettingsAreIgnored();
	TestDuplicateScan();
	TestInvalidPackageDoesNotFailScan();

	if (gFailures != 0)
	{
		std::cerr << gFailures << " ShaderPackageRegistry test failure(s).\n";
		return 1;
	}

	std::cout << "ShaderPackageRegistry tests passed.\n";
	return 0;
}
