#include "RuntimeLayerModel.h"

#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>

namespace
{
int gFailures = 0;

void Expect(bool condition, const std::string& message)
{
	if (condition)
		return;

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

std::filesystem::path MakeTestRoot()
{
	const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
	const std::filesystem::path root = std::filesystem::temp_directory_path() / ("render-cadence-layer-model-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;
}

std::string SolidShaderManifest(double gainDefault, bool includeMix)
{
	std::string parameters =
		"{ \"id\": \"gain\", \"label\": \"Gain\", \"type\": \"float\", \"default\": " + std::to_string(gainDefault) + " },\n"
		"\t\t\t{ \"id\": \"drop\", \"label\": \"Drop\", \"type\": \"trigger\" }";
	if (includeMix)
		parameters += ",\n\t\t\t{ \"id\": \"mix\", \"label\": \"Mix\", \"type\": \"float\", \"default\": 0.25 }";

	return R"({
		"id": "solid",
		"name": "Solid",
		"description": "Solid test shader",
		"category": "Tests",
		"entryPoint": "shadeVideo",
		"parameters": [
			)" + parameters + R"(
		]
	})";
}

std::string AllParametersShaderManifest()
{
	return R"({
		"id": "all-params",
		"name": "All Params",
		"description": "All parameter restore test shader",
		"category": "Tests",
		"entryPoint": "shadeVideo",
		"fonts": [{ "id": "inter", "path": "Inter.ttf" }],
		"parameters": [
			{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0.0, "max": 1.0 },
			{ "id": "offset", "label": "Offset", "type": "vec2", "default": [0.0, 0.0], "min": [-1.0, -1.0], "max": [1.0, 1.0] },
			{ "id": "tint", "label": "Tint", "type": "color", "default": [1.0, 1.0, 1.0, 1.0], "min": [0.0, 0.0, 0.0, 0.0], "max": [1.0, 1.0, 1.0, 1.0] },
			{ "id": "enabled", "label": "Enabled", "type": "bool", "default": true },
			{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
				{ "value": "soft", "label": "Soft" },
				{ "value": "hard", "label": "Hard" }
			] },
			{ "id": "titleText", "label": "Title", "type": "text", "default": "DEFAULT", "font": "inter", "maxLength": 8 },
			{ "id": "drop", "label": "Drop", "type": "trigger" }
		]
	})";
}

RenderCadenceCompositor::SupportedShaderCatalog LoadCatalog(const std::filesystem::path& root)
{
	RenderCadenceCompositor::SupportedShaderCatalog catalog;
	std::string error;
	Expect(catalog.Load(root, 4, error), error.empty() ? "catalog loads test shader" : error);
	return catalog;
}

RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::path& root)
{
	root = MakeTestRoot();
	WriteFile(root / "solid" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
	WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.5, false));
	return LoadCatalog(root);
}

RenderCadenceCompositor::FontAtlasBuildOutput MakeFakeFontAtlas()
{
	RenderCadenceCompositor::FontAtlasBuildOutput atlas;
	atlas.fontId = "inter";
	atlas.width = 2;
	atlas.height = 2;
	atlas.ascender = -0.8;
	atlas.rgbaPixels = {
		255, 255, 255, 255,
		255, 255, 255, 255,
		255, 255, 255, 255,
		255, 255, 255, 255
	};

	RenderCadenceCompositor::FontAtlasBuildOutput::Glyph glyph;
	glyph.advance = 0.5;
	glyph.planeLeft = 0.0;
	glyph.planeTop = 0.0;
	glyph.planeRight = 0.4;
	glyph.planeBottom = 0.6;
	glyph.atlasLeft = 0.0;
	glyph.atlasTop = 0.0;
	glyph.atlasRight = 1.0;
	glyph.atlasBottom = 1.0;
	glyph.hasBounds = true;
	atlas.glyphsByCodepoint['A'] = glyph;
	atlas.glyphsByCodepoint['B'] = glyph;
	atlas.glyphsByCodepoint['D'] = glyph;
	atlas.glyphsByCodepoint['E'] = glyph;
	atlas.glyphsByCodepoint['F'] = glyph;
	atlas.glyphsByCodepoint['L'] = glyph;
	atlas.glyphsByCodepoint['T'] = glyph;
	atlas.glyphsByCodepoint['U'] = glyph;
	return atlas;
}

void TestSingleLayerLifecycle()
{
	std::filesystem::path root;
	RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	Expect(model.InitializeSingleLayer(catalog, "solid", error), "model initializes a supported startup shader");
	Expect(model.FirstLayerId() == "runtime-layer-1", "startup layer id is stable");

	Expect(model.MarkBuildStarted(model.FirstLayerId(), "build started", error), "build start updates the layer");
	RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	Expect(snapshot.displayLayers.size() == 1, "snapshot exposes the display layer");
	Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Pending, "started layer is pending");
	Expect(!snapshot.displayLayers[0].renderReady, "started layer is not render-ready yet");

	RuntimeShaderArtifact artifact;
	artifact.shaderId = "solid";
	artifact.displayName = "Solid";
	artifact.fragmentShaderSource = "void main(){}";
	artifact.message = "build ready";
	Expect(model.MarkBuildReady(artifact, error), "ready artifact updates the matching layer");

	snapshot = model.Snapshot();
	Expect(snapshot.compileSucceeded, "ready layer reports compile success");
	Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Ready, "ready layer is marked ready");
	Expect(snapshot.displayLayers[0].renderReady, "ready layer exposes render readiness");
	Expect(snapshot.renderLayers.size() == 1, "ready layer produces one render layer artifact");
	Expect(snapshot.renderLayers[0].artifact.shaderId == "solid", "render layer carries the artifact");

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

void TestRejectsUnsupportedStartupShader()
{
	std::filesystem::path root;
	RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	Expect(!model.InitializeSingleLayer(catalog, "missing", error), "model rejects unsupported shader ids");
	Expect(!error.empty(), "unsupported shader rejection explains the problem");
	Expect(model.Snapshot().displayLayers.empty(), "rejected startup shader leaves no display layer");

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

void TestBuildFailureStaysDisplaySide()
{
	std::filesystem::path root;
	RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	Expect(model.InitializeSingleLayer(catalog, "solid", error), "model initializes for failure test");
	Expect(model.MarkBuildFailed(model.FirstLayerId(), "compile failed", error), "build failure updates the layer");

	const RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	Expect(!snapshot.compileSucceeded, "failed layer reports compile failure");
	Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Failed, "failed layer is marked failed");
	Expect(!snapshot.displayLayers[0].renderReady, "failed layer is not render-ready");
	Expect(snapshot.renderLayers.empty(), "failed layer does not produce a render artifact");

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

void TestAddAndRemoveLayers()
{
	std::filesystem::path root;
	RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	std::string firstLayerId;
	std::string secondLayerId;
	Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first layer can be added");
	Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second layer can be added");
	Expect(firstLayerId != secondLayerId, "added layers receive distinct ids");
	Expect(model.Snapshot().displayLayers.size() == 2, "added layers appear in display snapshot");

	Expect(model.RemoveLayer(firstLayerId, error), "existing layer can be removed");
	RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	Expect(snapshot.displayLayers.size() == 1, "removed layer leaves snapshot");
	Expect(snapshot.displayLayers[0].id == secondLayerId, "remaining layer identity is preserved");
	Expect(!model.RemoveLayer(firstLayerId, error), "removed layer cannot be removed twice");

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

void TestInitializeFromRuntimeStateRestoresLayerStack()
{
	std::filesystem::path root = MakeTestRoot();
	WriteFile(root / "solid" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
	WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.5, false));
	WriteFile(root / "all-params" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
	WriteFile(root / "all-params" / "Inter.ttf", "not a real font, but enough for restore catalog support checks");
	WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest());
	RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root);

	JsonValue runtimeState;
	std::string error;
	Expect(ParseJson(R"({
		"layers": [
			{
				"id": "layer-31",
				"shaderId": "all-params",
				"bypass": true,
				"parameterValues": {
					"gain": 0.75,
					"offset": [0.25, -0.5],
					"tint": [0.1, 0.2, 0.3, 0.4],
					"enabled": false,
					"mode": "hard",
					"titleText": "RESTORED-TEXT",
					"drop": 4
				}
			},
			{
				"id": "layer-32",
				"shaderId": "missing",
				"parameterValues": {
					"gain": 0.9
				}
			},
			{
				"id": "layer-33",
				"shaderId": "solid",
				"parameterValues": {
					"gain": "bad"
				}
			}
		]
	})", runtimeState, error), "runtime state fixture parses");

	RenderCadenceCompositor::RuntimeLayerModel model;
	Expect(model.InitializeFromRuntimeState(catalog, runtimeState, error), "runtime state can initialize the layer model");
	RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	Expect(snapshot.displayLayers.size() == 2, "restore keeps supported layers and skips missing shaders");
	Expect(snapshot.displayLayers[0].id == "layer-31", "restore preserves saved layer id");
	Expect(snapshot.displayLayers[0].shaderId == "all-params", "restore preserves shader id");
	Expect(snapshot.displayLayers[0].bypass, "restore preserves bypass state");
	Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.75, "restore preserves valid parameter values");
	Expect(snapshot.displayLayers[0].parameterValues.at("offset").numberValues == std::vector<double>({ 0.25, -0.5 }), "restore preserves vec2 parameter values");
	Expect(snapshot.displayLayers[0].parameterValues.at("tint").numberValues == std::vector<double>({ 0.1, 0.2, 0.3, 0.4 }), "restore preserves color parameter values");
	Expect(!snapshot.displayLayers[0].parameterValues.at("enabled").booleanValue, "restore preserves boolean parameter values");
	Expect(snapshot.displayLayers[0].parameterValues.at("mode").enumValue == "hard", "restore preserves enum parameter values");
	Expect(snapshot.displayLayers[0].parameterValues.at("titleText").textValue == "RESTORED", "restore normalizes and preserves text parameter values");
	Expect(snapshot.displayLayers[0].parameterValues.at("drop").numberValues.front() == 4.0, "restore preserves trigger counts");
	Expect(snapshot.displayLayers[1].id == "layer-33", "restore preserves later supported layer order");
	Expect(snapshot.displayLayers[1].shaderId == "solid", "restore preserves later layer shader id");
	Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "restore falls back to defaults for invalid parameter values");

	const std::vector<std::pair<std::string, std::string>> builds = model.PendingLayerBuilds();
	Expect(builds.size() == 2 && builds[0].first == "layer-31" && builds[0].second == "all-params" && builds[1].first == "layer-33" && builds[1].second == "solid", "restore queues startup builds for every restored layer in order");

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

void TestInvalidRuntimeStateCanFallBackToConfiguredShader()
{
	std::filesystem::path root;
	RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);

	JsonValue invalidRuntimeState = JsonValue::MakeObject();
	invalidRuntimeState.set("layers", JsonValue("not-an-array"));

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	Expect(!model.InitializeFromRuntimeState(catalog, invalidRuntimeState, error), "invalid runtime state is rejected");
	Expect(model.InitializeSingleLayer(catalog, "solid", error), "configured default shader can initialize after invalid runtime state");

	RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	Expect(snapshot.displayLayers.size() == 1, "fallback startup creates one default layer");
	Expect(snapshot.displayLayers[0].shaderId == "solid", "fallback layer uses the configured shader id");
	Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.5, "fallback layer uses shader defaults");

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

void TestLayerControlsUpdateDisplayAndRenderModels()
{
	std::filesystem::path root;
	RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	std::string firstLayerId;
	std::string secondLayerId;
	Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first control layer can be added");
	Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second control layer can be added");

	Expect(model.SetLayerBypass(firstLayerId, true, error), "bypass can be set");
	Expect(model.ReorderLayer(firstLayerId, 1, error), "layer can be reordered");
	RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	Expect(snapshot.displayLayers[1].id == firstLayerId, "reordered layer moves to requested index");
	Expect(snapshot.displayLayers[1].bypass, "bypass state is visible in read model");

	JsonValue gainValue(0.75);
	Expect(model.UpdateParameter(firstLayerId, "gain", gainValue, error), "parameter value can be updated");
	snapshot = model.Snapshot();
	Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.75, "updated parameter value is visible");
	JsonValue dropPulse(true);
	Expect(model.UpdateParameter(firstLayerId, "drop", dropPulse, error), "trigger parameter can be pulsed");
	snapshot = model.Snapshot();
	const std::vector<double> firstTrigger = snapshot.displayLayers[1].parameterValues.at("drop").numberValues;
	Expect(firstTrigger.size() == 2 && firstTrigger[0] == 1.0 && firstTrigger[1] >= 0.0, "trigger pulse increments count and records runtime time");
	Expect(model.UpdateParameter(firstLayerId, "drop", dropPulse, error), "trigger parameter can be pulsed again");
	snapshot = model.Snapshot();
	const std::vector<double> secondTrigger = snapshot.displayLayers[1].parameterValues.at("drop").numberValues;
	Expect(secondTrigger.size() == 2 && secondTrigger[0] == 2.0 && secondTrigger[1] >= firstTrigger[1], "second trigger pulse increments count again");

	RuntimeShaderArtifact artifact;
	artifact.layerId = firstLayerId;
	artifact.shaderId = "solid";
	artifact.displayName = "Solid";
	artifact.fragmentShaderSource = "void main(){}";
	artifact.parameterDefinitions = snapshot.displayLayers[1].parameterDefinitions;
	artifact.message = "build ready";
	Expect(model.MarkBuildReady(artifact, error), "ready artifact keeps layer parameter state");
	snapshot = model.Snapshot();
	Expect(snapshot.renderLayers.size() == 1, "ready layer produces render model");
	Expect(snapshot.renderLayers[0].bypass, "render model carries bypass state");
	Expect(snapshot.renderLayers[0].artifact.parameterValues.at("gain").numberValues.front() == 0.75, "render artifact carries updated parameter value");

	Expect(model.ResetParameters(firstLayerId, error), "parameters can reset to defaults");
	snapshot = model.Snapshot();
	Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "reset restores default value");

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

void TestReloadRefreshesChangedShaderMetadataAndPreservesValues()
{
	std::filesystem::path root;
	RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
	WriteFile(root / "passthrough" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 1.0, 1.0); }\n");
	WriteFile(root / "passthrough" / "shader.json", R"({
		"id": "passthrough",
		"name": "Passthrough",
		"description": "Passthrough test shader",
		"category": "Tests",
		"entryPoint": "shadeVideo",
		"parameters": [
			{ "id": "opacity", "label": "Opacity", "type": "float", "default": 1.0 }
		]
	})");
	catalog = LoadCatalog(root);

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	std::string layerId;
	std::string unchangedLayerId;
	Expect(model.AddLayer(catalog, "solid", layerId, error), "reload test layer can be added");
	Expect(model.AddLayer(catalog, "passthrough", unchangedLayerId, error), "reload unchanged layer can be added");
	Expect(model.UpdateParameter(layerId, "gain", JsonValue(0.75), error), "reload test parameter can be customized");

	WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.1, true));
	RenderCadenceCompositor::SupportedShaderCatalog reloadedCatalog = LoadCatalog(root);
	std::vector<std::pair<std::string, std::string>> buildsToStart;
	Expect(model.ReloadFromCatalog(reloadedCatalog, buildsToStart, error), "reload refreshes model from changed catalog");
	Expect(buildsToStart.size() == 2
		&& buildsToStart[0].first == layerId
		&& buildsToStart[0].second == "solid"
		&& buildsToStart[1].first == unchangedLayerId
		&& buildsToStart[1].second == "passthrough", "reload queues every available layer rebuild in order");

	RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	Expect(snapshot.displayLayers[0].parameterDefinitions.size() == 3, "reload exposes new JSON parameter definitions to UI");
	Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.75, "reload preserves compatible parameter values");
	Expect(snapshot.displayLayers[0].parameterValues.at("mix").numberValues.front() == 0.25, "reload initializes newly added parameters");
	Expect(snapshot.displayLayers[1].parameterValues.at("opacity").numberValues.front() == 1.0, "reload keeps unchanged layer parameter values");

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

RuntimeShaderArtifact MakeReadyArtifact(
	const RenderCadenceCompositor::SupportedShaderCatalog& catalog,
	const std::string& layerId,
	const std::string& shaderId,
	const std::string& sourceToken)
{
	const ShaderPackage* shaderPackage = catalog.FindPackage(shaderId);
	RuntimeShaderArtifact artifact;
	artifact.layerId = layerId;
	artifact.shaderId = shaderId;
	artifact.displayName = shaderPackage ? shaderPackage->displayName : shaderId;
	artifact.packageFingerprint = shaderPackage ? RenderCadenceCompositor::ShaderPackageFingerprint(*shaderPackage) : sourceToken;
	artifact.fragmentShaderSource = "void main(){/*" + sourceToken + "*/}";
	if (shaderPackage)
		artifact.parameterDefinitions = shaderPackage->parameters;
	artifact.message = "build ready";
	return artifact;
}

void TestReloadRebuildKeepsLastGoodRenderArtifacts()
{
	std::filesystem::path root;
	RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
	WriteFile(root / "passthrough" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 1.0, 1.0); }\n");
	WriteFile(root / "passthrough" / "shader.json", R"({
		"id": "passthrough",
		"name": "Passthrough",
		"description": "Passthrough test shader",
		"category": "Tests",
		"entryPoint": "shadeVideo",
		"parameters": [
			{ "id": "opacity", "label": "Opacity", "type": "float", "default": 1.0 }
		]
	})");
	catalog = LoadCatalog(root);

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	std::string solidLayerId;
	std::string passthroughLayerId;
	Expect(model.AddLayer(catalog, "solid", solidLayerId, error), "reload preserve solid layer can be added");
	Expect(model.AddLayer(catalog, "passthrough", passthroughLayerId, error), "reload preserve passthrough layer can be added");
	Expect(model.MarkBuildReady(MakeReadyArtifact(catalog, solidLayerId, "solid", "solid-old"), error), "solid layer starts render-ready");
	Expect(model.MarkBuildReady(MakeReadyArtifact(catalog, passthroughLayerId, "passthrough", "passthrough-old"), error), "passthrough layer starts render-ready");

	RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	Expect(snapshot.renderLayers.size() == 2, "ready stack exposes both render layers before reload");
	const std::string oldSolidSource = snapshot.renderLayers[0].artifact.fragmentShaderSource;
	const std::string oldPassthroughSource = snapshot.renderLayers[1].artifact.fragmentShaderSource;

	WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.25, true));
	RenderCadenceCompositor::SupportedShaderCatalog reloadedCatalog = LoadCatalog(root);
	std::vector<std::pair<std::string, std::string>> buildsToStart;
	Expect(model.ReloadFromCatalog(reloadedCatalog, buildsToStart, error), "reload preserve refreshes catalog");
	for (const auto& build : buildsToStart)
		Expect(model.MarkBuildStarted(build.first, "reload build started", error, true), "reload build preserves last good artifact");

	snapshot = model.Snapshot();
	Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Pending, "reload marks first layer pending");
	Expect(snapshot.renderLayers.size() == 2, "pending reload keeps full render stack");
	Expect(snapshot.renderLayers[0].artifact.fragmentShaderSource == oldSolidSource, "pending reload keeps old first layer artifact");
	Expect(snapshot.renderLayers[1].artifact.fragmentShaderSource == oldPassthroughSource, "pending reload keeps old second layer artifact");

	Expect(model.MarkBuildFailed(solidLayerId, "reload compile failed", error), "failed reload marks layer failed");
	snapshot = model.Snapshot();
	Expect(!snapshot.compileSucceeded, "failed reload reports compile failure");
	Expect(snapshot.renderLayers.size() == 2, "failed reload keeps last good render stack");
	Expect(snapshot.renderLayers[0].artifact.fragmentShaderSource == oldSolidSource, "failed reload keeps old failed layer artifact");

	Expect(model.MarkBuildReady(MakeReadyArtifact(reloadedCatalog, passthroughLayerId, "passthrough", "passthrough-new"), error), "other reload layer can commit");
	snapshot = model.Snapshot();
	Expect(snapshot.renderLayers.size() == 2, "partial reload commit still keeps complete render stack");
	Expect(snapshot.renderLayers[0].artifact.fragmentShaderSource == oldSolidSource, "partial reload commit keeps old failed layer artifact");
	Expect(snapshot.renderLayers[1].artifact.fragmentShaderSource.find("passthrough-new") != std::string::npos, "partial reload commit updates ready layer");

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

void TestTextTexturesArePreparedInRuntimeModel()
{
	std::filesystem::path root = MakeTestRoot();
	WriteFile(root / "all-params" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
	WriteFile(root / "all-params" / "Inter.ttf", "not a real font, but enough for catalog support checks");
	WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest());
	RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root);

	RenderCadenceCompositor::RuntimeLayerModel model;
	std::string error;
	Expect(model.InitializeSingleLayer(catalog, "all-params", error), "text layer can initialize");

	RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
	RuntimeShaderArtifact artifact;
	artifact.layerId = model.FirstLayerId();
	artifact.shaderId = "all-params";
	artifact.displayName = "All Params";
	artifact.fragmentShaderSource = "void main(){}";
	artifact.parameterDefinitions = snapshot.displayLayers[0].parameterDefinitions;
	artifact.fontAtlases.push_back(MakeFakeFontAtlas());
	artifact.message = "build ready";
	Expect(model.MarkBuildReady(artifact, error), error.empty() ? "ready text artifact prepares textures" : error);

	snapshot = model.Snapshot();
	Expect(snapshot.renderLayers.size() == 1, "text artifact is render-ready");
	Expect(snapshot.renderLayers[0].artifact.preparedTextTextures.size() == 1, "render snapshot carries prepared text texture");
	Expect(snapshot.renderLayers[0].artifact.fontAtlases.empty(), "render snapshot does not carry font atlas pixels");
	const RuntimePreparedTextTexture preparedDefault = snapshot.renderLayers[0].artifact.preparedTextTextures[0];
	Expect(preparedDefault.textValue == "DEFAULT", "default text is prepared");
	Expect(preparedDefault.rgbaPixels && !preparedDefault.rgbaPixels->empty(), "prepared text has pixels");

	Expect(model.UpdateParameter(model.FirstLayerId(), "titleText", JsonValue("AB"), error), error.empty() ? "text parameter update prepares texture" : error);
	snapshot = model.Snapshot();
	const RuntimePreparedTextTexture preparedUpdated = snapshot.renderLayers[0].artifact.preparedTextTextures[0];
	Expect(preparedUpdated.textValue == "AB", "updated text is prepared before render snapshot");
	Expect(preparedUpdated.rgbaPixels && preparedUpdated.rgbaPixels != preparedDefault.rgbaPixels, "updated text receives a new prepared pixel payload");

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

int main()
{
	TestSingleLayerLifecycle();
	TestRejectsUnsupportedStartupShader();
	TestBuildFailureStaysDisplaySide();
	TestAddAndRemoveLayers();
	TestInitializeFromRuntimeStateRestoresLayerStack();
	TestInvalidRuntimeStateCanFallBackToConfiguredShader();
	TestLayerControlsUpdateDisplayAndRenderModels();
	TestReloadRefreshesChangedShaderMetadataAndPreservesValues();
	TestReloadRebuildKeepsLastGoodRenderArtifacts();
	TestTextTexturesArePreparedInRuntimeModel();

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

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