/***********************************************************************
THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY
DEVELOPER: Zihan Chen(vczh)
***********************************************************************/
#include "GacUI.UnitTest.h"

/***********************************************************************
.\GUIUNITTESTPROTOCOL_RENDERING.CPP
***********************************************************************/

namespace vl::presentation::unittest
{
	using namespace collections;
	using namespace remoteprotocol;

/***********************************************************************
IGuiRemoteProtocolMessages (Rendering)
***********************************************************************/

	Ptr<UnitTestLoggedFrame> UnitTestRemoteProtocol_Rendering::GetLastRenderingFrame()
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::GetLastRenderingCommands()#"
		CHECK_ERROR(lastRenderingCommandsOpening, ERROR_MESSAGE_PREFIX L"The latest frame of commands is not accepting new commands.");
		return loggedFrames[loggedFrames.Count() - 1];
#undef ERROR_MESSAGE_PREFIX
	}
		
	Ptr<UnitTestLoggedFrame> UnitTestRemoteProtocol_Rendering::TryGetLastRenderingFrameAndReset()
	{
		if (loggedFrames.Count() == 0) return nullptr;
		if (!lastRenderingCommandsOpening) return nullptr;
		lastRenderingCommandsOpening = false;
		return loggedFrames[loggedFrames.Count() - 1];
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererBeginRendering(const ElementBeginRendering& arguments)
	{
		receivedDomDiffMessage = false;
		receivedElementMessage = false;
		lastRenderingCommandsOpening = true;

		auto frame = Ptr(new UnitTestLoggedFrame);
		frame->frameId = arguments.frameId;
		loggedFrames.Add(frame);

		if (arguments.updatedElements && arguments.updatedElements.Obj()->Count() > 0)
		{
			// Apply element updates first so event logs remain backward-compatible:
			// Updated(...), Updated(...), ..., Begin()
			for (auto&& desc : *arguments.updatedElements.Obj())
			{
				desc.Apply(Overloading(
					[&](const ElementDesc_SolidBorder& d) { Impl_RendererUpdateElement_SolidBorder(d); },
					[&](const ElementDesc_SinkBorder& d) { Impl_RendererUpdateElement_SinkBorder(d); },
					[&](const ElementDesc_SinkSplitter& d) { Impl_RendererUpdateElement_SinkSplitter(d); },
					[&](const ElementDesc_SolidBackground& d) { Impl_RendererUpdateElement_SolidBackground(d); },
					[&](const ElementDesc_GradientBackground& d) { Impl_RendererUpdateElement_GradientBackground(d); },
					[&](const ElementDesc_InnerShadow& d) { Impl_RendererUpdateElement_InnerShadow(d); },
					[&](const ElementDesc_Polygon& d) { Impl_RendererUpdateElement_Polygon(d); },
					[&](const ElementDesc_SolidLabel& d) { Impl_RendererUpdateElement_SolidLabel(d); },
					[&](const ElementDesc_ImageFrame& d) { Impl_RendererUpdateElement_ImageFrame(d); }
				));
			}

			renderingDomBuilder.RequestRendererBeginRendering();
		}
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererEndRendering(vint id)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererEndRendering(vint)#"
		CHECK_ERROR(receivedElementMessage || receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"Either dom-diff or element message should be sent before this message.");

		auto lastFrame = GetLastRenderingFrame();
		if (receivedElementMessage)
		{
			lastFrame->renderingDom = renderingDomBuilder.RequestRendererEndRendering();
		}
		if (receivedDomDiffMessage)
		{
			// receivedDom will be updated in RequestRendererRenderDomDiff
			// store a copy to log
			lastFrame->renderingDom = CopyDom(receivedDom);
		}
		this->GetEvents()->RespondRendererEndRendering(id, measuringForNextRendering);
		measuringForNextRendering = {};
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererIdle()
	{
		rendererIdleCount++;
	}

/***********************************************************************
IGuiRemoteProtocolMessages (Rendering - Element)
***********************************************************************/

	void UnitTestRemoteProtocol_Rendering::Impl_RendererBeginBoundary(const ElementBoundary& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererBeginBoundary(const ElementBoundary&)#"
		CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle.");
		if (!receivedElementMessage)
		{
			renderingDomBuilder.RequestRendererBeginRendering();
			receivedElementMessage = true;
		}

		renderingDomBuilder.RequestRendererBeginBoundary(arguments);

		glr::json::JsonFormatting formatting;
		formatting.spaceAfterColon = true;
		formatting.spaceAfterComma = true;
		formatting.crlf = false;
		formatting.compact = true;

		GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererBeginBoundary: " + stream::GenerateToStream([&](stream::TextWriter& writer)
		{
			auto jsonLog = ConvertCustomTypeToJson(arguments);
			writer.WriteString(glr::json::JsonToString(jsonLog, formatting));
		}));
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererEndBoundary()
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererEndBoundary()#"
		CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle.");
		if (!receivedElementMessage)
		{
			renderingDomBuilder.RequestRendererBeginRendering();
			receivedElementMessage = true;
		}

		renderingDomBuilder.RequestRendererEndBoundary();
		GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererEndBoundary");
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::EnsureRenderedElement(vint id, Rect bounds)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::EnsureRenderedElement(id&)#"

		vint index = loggedTrace.createdElements->Keys().IndexOf(id);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");
		auto rendererType = loggedTrace.createdElements->Values()[index];
		if (rendererType == RendererType::FocusRectangle || rendererType == RendererType::Raw)
		{
			// FocusRectangle or Raw does not have a ElementDesc
			return;
		}

		index = lastElementDescs.Keys().IndexOf(id);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been updated after created.");
		lastElementDescs.Values()[index].Apply(Overloading(
			[](RendererType)
			{
				CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been updated after created.");
			},
			[&](const ElementDesc_SolidLabel& solidLabel)
			{
				CalculateSolidLabelSizeIfNecessary(bounds.Width(), bounds.Height(), solidLabel);
			},
			[&](const auto& element)
			{
			}));

#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const ElementRendering& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const ElementRendering&)#"
		CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle.");
		if (!receivedElementMessage)
		{
			renderingDomBuilder.RequestRendererBeginRendering();
			receivedElementMessage = true;
		}

		{
			renderingDomBuilder.RequestRendererRenderElement(arguments);

			glr::json::JsonFormatting formatting;
			formatting.spaceAfterColon = true;
			formatting.spaceAfterComma = true;
			formatting.crlf = false;
			formatting.compact = true;
			GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererRenderElement: " + stream::GenerateToStream([&](stream::TextWriter& writer)
			{
				auto jsonLog = ConvertCustomTypeToJson(arguments);
				writer.WriteString(glr::json::JsonToString(jsonLog, formatting));
			}));
		}

		EnsureRenderedElement(arguments.id, arguments.bounds);
#undef ERROR_MESSAGE_PREFIX
	}

/***********************************************************************
IGuiRemoteProtocolMessages (Rendering - Dom)
***********************************************************************/

	void UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizesIfNecessary(Ptr<RenderingDom> dom)
	{
		if (dom->content.element)
		{
			EnsureRenderedElement(dom->content.element.Value(), dom->content.bounds);
		}
		if (dom->children)
		{
			for (auto child : *dom->children.Obj())
			{
				CalculateSolidLabelSizesIfNecessary(child);
			}
		}
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderDom(const Ptr<RenderingDom>& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const RenderingDom&)#"
		CHECK_ERROR(!receivedElementMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with element messages in the same rendering cycle.");
		if (!receivedDomDiffMessage)
		{
			receivedDomDiffMessage = true;
		}

		receivedDom = arguments;
		BuildDomIndex(receivedDom, receivedDomIndex);
		CalculateSolidLabelSizesIfNecessary(receivedDom);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderDomDiff(const RenderingDom_DiffsInOrder& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const RenderingDom_DiffsInOrder&)#"
		CHECK_ERROR(!receivedElementMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with element messages in the same rendering cycle.");
		if (!receivedDomDiffMessage)
		{
			receivedDomDiffMessage = true;
		}
		
		UpdateDomInplace(receivedDom, receivedDomIndex, arguments);
		GetLastRenderingFrame()->renderingDiffs = arguments;
		CalculateSolidLabelSizesIfNecessary(receivedDom);
#undef ERROR_MESSAGE_PREFIX
	}

/***********************************************************************
IGuiRemoteProtocolMessages (Elements)
***********************************************************************/

	void UnitTestRemoteProtocol_Rendering::Impl_RendererCreated(const Ptr<List<RendererCreation>>& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererCreated(const Ptr<List<RendererCreation>>&)#"
		if (arguments)
		{
			for (auto creation : *arguments.Obj())
			{
				CHECK_ERROR(!loggedTrace.createdElements->Keys().Contains(creation.id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has been created or used.");
				loggedTrace.createdElements->Add(creation.id, creation.type);

				// Create paragraph state for DocumentParagraph elements
				if (creation.type == RendererType::DocumentParagraph)
				{
					auto state = Ptr(new DocumentParagraphState());
					paragraphStates.Add(creation.id, state);
				}
			}
		}
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererDestroyed(const Ptr<List<vint>>& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererDestroyed(const Ptr<List<vint>>&)#"
		if (arguments)
		{
			for (auto id : *arguments.Obj())
			{
				CHECK_ERROR(loggedTrace.createdElements->Keys().Contains(id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");
				CHECK_ERROR(!removedElementIds.Contains(id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has been destroyed.");
				removedElementIds.Add(id);
				lastElementDescs.Remove(id);

				// Remove paragraph state if this was a DocumentParagraph
				paragraphStates.Remove(id);
			}
		}
#undef ERROR_MESSAGE_PREFIX
	}

#define REQUEST_RENDERER_UPDATE_ELEMENT2(ARGUMENTS, RENDERER_TYPE)\
		RequestRendererUpdateElement<RENDERER_TYPE>(\
			ARGUMENTS,\
			ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.",\
			ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."\
			)

#define REQUEST_RENDERER_UPDATE_ELEMENT(RENDERER_TYPE) REQUEST_RENDERER_UPDATE_ELEMENT2(arguments, RENDERER_TYPE)

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBorder(const ElementDesc_SolidBorder& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBorder(const ElementDesc_SolidBorder&)#"
		REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SolidBorder);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkBorder(const ElementDesc_SinkBorder& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkBorder(const ElementDesc_SinkBorder&)#"
		REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SinkBorder);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkSplitter(const ElementDesc_SinkSplitter& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkSplitter(const ElementDesc_SinkSplitter&)#"
		REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SinkSplitter);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBackground(const ElementDesc_SolidBackground& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBackground(const ElementDesc_SolidBackground&)#"
		REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SolidBackground);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_GradientBackground(const ElementDesc_GradientBackground& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_GradientBackground(const ElementDesc_GradientBackground&)#"
		REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::GradientBackground);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_InnerShadow(const ElementDesc_InnerShadow& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_InnerShadow(const ElementDesc_InnerShadow&)#"
		REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::InnerShadow);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_Polygon(const ElementDesc_Polygon& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_Polygon(const ElementDesc_Polygon&)#"
		REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::Polygon);
#undef ERROR_MESSAGE_PREFIX
	}

/***********************************************************************
IGuiRemoteProtocolMessages (Elements - SolidLabel)
***********************************************************************/

	void UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizeIfNecessary(vint width, vint height, const ElementDesc_SolidLabel& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizeIfNecessary(vint, vint, const ElementDesc_SolidLabel&)#"

		if (arguments.measuringRequest)
		{
			switch (arguments.measuringRequest.Value())
			{
			case ElementSolidLabelMeasuringRequest::FontHeight:
				CHECK_ERROR(arguments.font, ERROR_MESSAGE_PREFIX L"Font is missing for calculating font height.");
				if (!measuringForNextRendering.fontHeights)
				{
					measuringForNextRendering.fontHeights = Ptr(new List<ElementMeasuring_FontHeight>);
				}
				{
					ElementMeasuring_FontHeight measuring;
					measuring.fontFamily = arguments.font.Value().fontFamily;
					measuring.fontSize = arguments.font.Value().size;
					measuring.height = measuring.fontSize + 4;
					measuringForNextRendering.fontHeights->Add(measuring);
				}
				break;
			case ElementSolidLabelMeasuringRequest::TotalSize:
				{
					// font and text has already been verified exist in RequestRendererUpdateElement_SolidLabel
					vint size = arguments.font.Value().size;
					auto text = arguments.text.Value();
					vint textWidth = 0;
					vint textHeight = 0;

					List<WString> lines;
					{
						List<Ptr<regex::RegexMatch>> matches;
						regexCrLf.Split(text, true, matches);

						if (matches.Count() == 0)
						{
							// when there is no text, measure a space
							lines.Add(WString::Unmanaged(L" "));
						}
						else if (arguments.multiline)
						{
							// add all lines, and if any line is empty, measure a space
							for (auto match : matches)
							{
								auto line = match->Result().Value();
								lines.Add(line.Length() ? line : WString::Unmanaged(L" "));
							}
						}
						else
						{
							lines.Add(stream::GenerateToStream([&](stream::TextWriter& writer)
							{
								for (auto [match, index] : indexed(matches))
								{
									if (index > 0) writer.WriteChar(L' ');
									auto line = match->Result().Value();
									writer.WriteString(line);
								}
							}));
							if(lines[0].Length() == 0)
							{
								// when there is no text, measure a space
								lines[0] = WString::Unmanaged(L" ");
							}
						}
					}

					if (arguments.wrapLine)
					{
						// width of the text is 0
						// insert a line break when there is no space horizontally
						vint totalLines = 0;
						for (auto&& line : lines)
						{
							if (line.Length() == 0)
							{
								totalLines++;
								continue;
							}

							double accumulatedWidth = 0;
							for (vint i = 0; i < line.Length(); i++)
							{
								auto c = line[i];
								auto w = (c < 128 ? 0.6 : 1) * size;
								if (accumulatedWidth + w > width)
								{
									if (accumulatedWidth == 0)
									{
										totalLines++;
									}
									else
									{
										totalLines++;
										accumulatedWidth = w;
									}
								}
								else
								{
									accumulatedWidth += w;
								}
							}
							if (accumulatedWidth > 0)
							{
								totalLines++;
							}
						}
						textHeight = 4 + size * totalLines;
					}
					else
					{
						// width of the text is width of the longest line
						textWidth = (vint)(size * From(lines)
							.Select([](const WString& line)
							{
								double sum = 0;
								for (vint i = 0; i < line.Length(); i++)
								{
									auto c = line[i];
									sum += (c < 128 ? 0.6 : 1);
								}
								return sum;
							})
							.Max());
						textHeight = 4 + size * lines.Count();
					}

					if (!measuringForNextRendering.minSizes)
					{
						measuringForNextRendering.minSizes = Ptr(new List<ElementMeasuring_ElementMinSize>);
					}
					{
						ElementMeasuring_ElementMinSize measuring;
						measuring.id = arguments.id;
						measuring.minSize = { textWidth,textHeight };
						measuringForNextRendering.minSizes->Add(measuring);
					}
				}
				break;
			default:
				CHECK_FAIL(L"Unknown value of ElementSolidLabelMeasuringRequest.");
			}
		}
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidLabel(const ElementDesc_SolidLabel& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidLabel(const ElementDesc_SolidLabel&)#"
		auto element = arguments;
		if (!element.font || !element.text)
		{
			vint index = loggedTrace.createdElements->Keys().IndexOf(element.id);
			CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");

			auto rendererType = loggedTrace.createdElements->Values()[index];
			CHECK_ERROR(rendererType == RendererType::SolidLabel, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");

			index = lastElementDescs.Keys().IndexOf(arguments.id);
			if (index != -1)
			{
				auto solidLabel = lastElementDescs.Values()[index].TryGet<ElementDesc_SolidLabel>();
				CHECK_ERROR(solidLabel, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
				if (!element.font) element.font = solidLabel->font;
				if (!element.text) element.text = solidLabel->text;
			}
			else
			{
				if (!element.font) element.font = FontProperties();
				if (!element.text) element.text = WString::Empty;
			}
		}
		REQUEST_RENDERER_UPDATE_ELEMENT2(element, RendererType::SolidLabel);
#undef ERROR_MESSAGE_PREFIX
	}

/***********************************************************************
IGuiRemoteProtocolMessages (Elements - Image)
***********************************************************************/

	WString UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromBinary(stream::IStream& binary)
	{
		stream::MemoryStream base64WStringStream;
		{
			stream::UtfGeneralEncoder<wchar_t, char8_t> utf8ToWCharEncoder;
			stream::EncoderStream utf8ToWCharStream(base64WStringStream, utf8ToWCharEncoder);
			stream::Utf8Base64Encoder binaryToBase64Utf8Encoder;
			stream::EncoderStream binaryToBase64Utf8Stream(utf8ToWCharStream, binaryToBase64Utf8Encoder);
			binary.SeekFromBegin(0);
			stream::CopyStream(binary, binaryToBase64Utf8Stream);
		}
		{
			base64WStringStream.SeekFromBegin(0);
			stream::StreamReader reader(base64WStringStream);
			return reader.ReadToEnd();
		}
	}

	WString UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromImage(Ptr<INativeImage> image)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromImage(Ptr<INativeImage>)#"
		auto remoteImage = image.Cast<GuiRemoteGraphicsImage>();
		CHECK_ERROR(remoteImage, ERROR_MESSAGE_PREFIX L"The image object must be GuiRemoteGraphicsImage.");
		return GetBinaryKeyFromBinary(remoteImage->GetBinaryData());
#undef ERROR_MESSAGE_PREFIX
	}

	ImageMetadata UnitTestRemoteProtocol_Rendering::MakeImageMetadata(const ImageCreation& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::MakeImageMetadata(const remoteprotocol::ImageCreation)#"
		if (!cachedImageMetadatas)
		{
			cachedImageMetadatas = Ptr(new Base64ToImageMetadataMap);
			for (auto resource : GetResourceManager()->GetLoadedResources())
			{
				if (auto xmlImageData = resource->GetValueByPath(WString::Unmanaged(L"UnitTestConfig/ImageData")).Cast<glr::xml::XmlDocument>())
				{
					for (auto elementImage : glr::xml::XmlGetElements(xmlImageData->rootElement, WString::Unmanaged(L"Image")))
					{
						WString path, format, frames = WString::Unmanaged(L"1"), width, height;

						auto attPath = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Path"));
						auto attFormat = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Format"));
						auto attFrames = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Frames"));
						auto attWidth = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Width"));
						auto attHeight = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Height"));

						CHECK_ERROR(attPath, ERROR_MESSAGE_PREFIX L"Missing Path attribute in Image element in an UnitTestConfig/ImageData.");
						CHECK_ERROR(attFormat, ERROR_MESSAGE_PREFIX L"Missing Format attribute in Image element in an UnitTestConfig/ImageData.");
						CHECK_ERROR(attWidth, ERROR_MESSAGE_PREFIX L"Missing Width attribute in Image element in an UnitTestConfig/ImageData.");
						CHECK_ERROR(attHeight, ERROR_MESSAGE_PREFIX L"Missing Height attribute in Image element in an UnitTestConfig/ImageData.");

						path = attPath->value.value;
						format = attFormat->value.value;
						width = attWidth->value.value;
						height = attHeight->value.value;
						if (attFrames) frames = attFrames->value.value;

						vint valueFrames = wtoi(frames);
						vint valueWidth = wtoi(width);
						vint valueHeight = wtoi(height);

						CHECK_ERROR(itow(valueFrames) == frames, ERROR_MESSAGE_PREFIX L"Frames attribute must be an integer in Image element in an UnitTestConfig/ImageData.");
						CHECK_ERROR(itow(valueWidth) == width, ERROR_MESSAGE_PREFIX L"Width attribute must be an integer in Image element in an UnitTestConfig/ImageData.");
						CHECK_ERROR(itow(valueHeight) == height, ERROR_MESSAGE_PREFIX L"Height attribute must be an integer in Image element in an UnitTestConfig/ImageData.");

						auto imageData = resource->GetImageByPath(path);
						WString binaryKey = GetBinaryKeyFromImage(imageData->GetImage());

						if (!cachedImageMetadatas->Keys().Contains(binaryKey))
						{
							ImageMetadata imageMetadata;
							imageMetadata.id = -1;
							imageMetadata.frames = Ptr(new List<ImageFrameMetadata>);
							{
								auto node = Ptr(new glr::json::JsonString);
								node->content.value = format;
								ConvertJsonToCustomType(node, imageMetadata.format);
							}
							for (vint frame = 0; frame < valueFrames; frame++)
							{
								imageMetadata.frames->Add({ {valueWidth,valueHeight} });
							}

							cachedImageMetadatas->Add(binaryKey, imageMetadata);
						}
					}
				}
			}
		}

		auto binaryKey = GetBinaryKeyFromBinary(*arguments.imageData.Obj());
		vint binaryIndex = cachedImageMetadatas->Keys().IndexOf(binaryKey);
		CHECK_ERROR(binaryIndex != -1, ERROR_MESSAGE_PREFIX L"The image is not registered in any UnitTestConfig/ImageData.");
		auto metadata = cachedImageMetadatas->Values()[binaryIndex];
		metadata.id = arguments.id;

		loggedTrace.imageCreations->Add(arguments);
		loggedTrace.imageMetadatas->Add(metadata);
		return metadata;
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_ImageCreated(vint id, const ImageCreation& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_ImageCreated(vint, const vint&)#"
		CHECK_ERROR(!loggedTrace.imageMetadatas->Keys().Contains(arguments.id), ERROR_MESSAGE_PREFIX L"Image with the specified id has been created or used.");
		this->GetEvents()->RespondImageCreated(id, MakeImageMetadata(arguments));
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_ImageDestroyed(const vint& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_ImageDestroyed(const vint&)#"
		CHECK_ERROR(loggedTrace.imageMetadatas->Keys().Contains(arguments), ERROR_MESSAGE_PREFIX L"Image with the specified id has not been created.");
		CHECK_ERROR(!removedImageIds.Contains(arguments), ERROR_MESSAGE_PREFIX L"Image with the specified id has been destroyed.");
		removedImageIds.Add(arguments);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_ImageFrame(const ElementDesc_ImageFrame& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_ImageFrame(const ElementDesc_ImageFrame&)#"
		if (arguments.imageCreation)
		{
			auto&& imageCreation = arguments.imageCreation.Value();
			if (!imageCreation.imageDataOmitted)
			{
				CHECK_ERROR(arguments.imageId && arguments.imageId.Value() != !imageCreation.id, ERROR_MESSAGE_PREFIX L"It should satisfy that (arguments.imageId.Value()id == imageCreation.id).");
				CHECK_ERROR(!loggedTrace.imageMetadatas->Keys().Contains(imageCreation.id), ERROR_MESSAGE_PREFIX L"Image with the specified id has been created.");
				CHECK_ERROR(imageCreation.imageData, ERROR_MESSAGE_PREFIX L"When imageDataOmitted == false, imageData should not be null.");
				if (!measuringForNextRendering.createdImages)
				{
					measuringForNextRendering.createdImages = Ptr(new List<ImageMetadata>);
				}
				measuringForNextRendering.createdImages->Add(MakeImageMetadata(imageCreation));
			}
			else
			{
				CHECK_ERROR(!imageCreation.imageData, ERROR_MESSAGE_PREFIX L"When imageDataOmitted == true, imageData should be null.");
			}
		}
		else if (arguments.imageId)
		{
			CHECK_ERROR(loggedTrace.imageMetadatas->Keys().Contains(arguments.imageId.Value()), ERROR_MESSAGE_PREFIX L"Image with the specified id has not been created.");
		}

		auto element = arguments;
		element.imageCreation.Reset();
		REQUEST_RENDERER_UPDATE_ELEMENT2(element, RendererType::ImageFrame);
#undef ERROR_MESSAGE_PREFIX
	}

#undef REQUEST_RENDERER_UPDATE_ELEMENT
#undef REQUEST_RENDERER_UPDATE_ELEMENT2
}


/***********************************************************************
.\GUIUNITTESTPROTOCOL_RENDERING_DOCUMENT.CPP
***********************************************************************/

namespace vl::presentation::unittest
{
	using namespace collections;
	using namespace remoteprotocol;

/***********************************************************************
Helper Functions for Document Paragraph
***********************************************************************/

	constexpr vint DefaultFontSize = 12;
	constexpr vint DefaultLineHeight = 16;

	void ClearCaretLayouts(DocumentParagraphState& state)
	{
		state.caretLayouts.Clear();
		state.caretLayoutKeys.Clear();
	}

	void AddCaretLayout(DocumentParagraphState& state, vint caret, const DocumentParagraphCharLayout& layout)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::AddCaretLayout(DocumentParagraphState&, vint, const DocumentParagraphCharLayout&)#"
		if (state.caretLayoutKeys.Count() > 0)
		{
			CHECK_ERROR(state.caretLayoutKeys[state.caretLayoutKeys.Count() - 1] < caret, ERROR_MESSAGE_PREFIX L"Duplicate or out-of-order caret.");
		}
		state.caretLayoutKeys.Add(caret);
		state.caretLayouts.Add(caret, layout);
#undef ERROR_MESSAGE_PREFIX
	}

	vint FindLayoutCaretLE(const DocumentParagraphState& state, vint textPos)
	{
		if (state.caretLayoutKeys.Count() == 0) return -1;
		vint index = -1;
		auto comparer = [&](const vint& key, const vint& search) -> std::strong_ordering
		{
			return key <=> search;
		};
		auto hit = BinarySearchLambda(&state.caretLayoutKeys[0], state.caretLayoutKeys.Count(), textPos, index, comparer);
		if (hit != -1) return state.caretLayoutKeys[hit];
		if (index < 0) return -1;
		if (state.caretLayoutKeys[index] < textPos) return state.caretLayoutKeys[index];
		if (index == 0) return -1;
		return state.caretLayoutKeys[index - 1];
	}

	vint FindLayoutCaretGT(const DocumentParagraphState& state, vint textPos)
	{
		if (state.caretLayoutKeys.Count() == 0) return -1;
		vint index = -1;
		auto comparer = [&](const vint& key, const vint& search) -> std::strong_ordering
		{
			return key <=> search;
		};
		auto hit = BinarySearchLambda(&state.caretLayoutKeys[0], state.caretLayoutKeys.Count(), textPos, index, comparer);
		if (hit != -1)
		{
			if (hit + 1 < state.caretLayoutKeys.Count()) return state.caretLayoutKeys[hit + 1];
			return -1;
		}
		if (index < 0) return state.caretLayoutKeys[0];
		if (state.caretLayoutKeys[index] > textPos) return state.caretLayoutKeys[index];
		if (index + 1 < state.caretLayoutKeys.Count()) return state.caretLayoutKeys[index + 1];
		return -1;
	}

	vint FindFirstCaretKeyAtOrAfter(const DocumentParagraphState& state, vint textPos)
	{
		if (state.caretLayoutKeys.Count() == 0) return -1;
		vint index = -1;
		auto comparer = [&](const vint& key, const vint& search) -> std::strong_ordering
		{
			return key <=> search;
		};
		auto hit = BinarySearchLambda(&state.caretLayoutKeys[0], state.caretLayoutKeys.Count(), textPos, index, comparer);
		if (hit != -1) return hit;
		if (index < 0) return 0;
		if (state.caretLayoutKeys[index] < textPos)
		{
			if (index + 1 < state.caretLayoutKeys.Count()) return index + 1;
			return -1;
		}
		return index;
	}

	bool TryGetLayoutAtCaret(const DocumentParagraphState& state, vint caret, DocumentParagraphCharLayout& layout)
	{
		if (!state.caretLayoutKeys.Contains(caret)) return false;
		layout = state.caretLayouts[caret];
		return true;
	}

	bool TryGetInlineObjectRangeContaining(const DocumentParagraphState& state, vint caret, elements::CaretRange& range)
	{
		for (auto&& [inlineRange, _] : state.inlineObjectRuns)
		{
			if (caret > inlineRange.caretBegin && caret < inlineRange.caretEnd)
			{
				range = inlineRange;
				return true;
			}
		}
		return false;
	}

	bool TryGetInlineObjectRangeAtBegin(const DocumentParagraphState& state, vint caret, elements::CaretRange& range)
	{
		for (auto&& [inlineRange, _] : state.inlineObjectRuns)
		{
			if (caret == inlineRange.caretBegin)
			{
				range = inlineRange;
				return true;
			}
		}
		return false;
	}

	bool TryGetInlineObjectRangeAtEnd(const DocumentParagraphState& state, vint caret, elements::CaretRange& range)
	{
		for (auto&& [inlineRange, _] : state.inlineObjectRuns)
		{
			if (caret == inlineRange.caretEnd)
			{
				range = inlineRange;
				return true;
			}
		}
		return false;
	}

	bool IsValidCaretPosition(const DocumentParagraphState& state, vint caret)
	{
		if (caret < 0 || caret > state.text.Length()) return false;
		for (auto&& [range, _] : state.inlineObjectRuns)
		{
			if (caret > range.caretBegin && caret < range.caretEnd)
			{
				return false;
			}
		}
		return true;
	}

	vint GetFontSizeForPosition(
		const DocumentParagraphState& state,
		vint pos,
		Nullable<Pair<vint, DocumentInlineObjectRunProperty>>& inlineProp)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::GetFontSizeForPosition(...)#"
		inlineProp.Reset();

		for (auto [range, prop] : state.mergedRuns)
		{
			if (pos >= range.caretBegin && pos < range.caretEnd)
			{
				if (auto textProp = prop.TryGet<DocumentTextRunProperty>())
				{
					return textProp->fontProperties.size;
				}
				if (auto objProp = prop.TryGet<DocumentInlineObjectRunProperty>())
				{
					inlineProp = { range.caretEnd - range.caretBegin,*objProp };
					return 0;
				}
			}
		}
		CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Every character is expected to have a font.");
#undef ERROR_MESSAGE_PREFIX
	}

	double GetCharacterWidth(wchar_t c, vint fontSize)
	{
		return (c < 128 ? 0.6 : 1.0) * fontSize;
	}

	void CalculateParagraphLayout(DocumentParagraphState& state)
	{
		ClearCaretLayouts(state);
		state.lines.Clear();
		state.cachedSize = Size(0, DefaultLineHeight);
		state.cachedInlineObjectBounds.Clear();

		const WString& text = state.text;

		if (text.Length() == 0)
		{
			// Empty paragraph has default size
			DocumentParagraphLineInfo line;
			line.startPos = 0;
			line.endPos = 0;
			line.y = 0;
			line.height = DefaultLineHeight; // Default: 12 (font) + 4
			line.baseline = DefaultFontSize;
			line.width = 0;
			state.lines.Add(line);
			return;
		}

		// First pass: calculate per-character metrics
		struct TempCharInfo
		{
			vint caret;
			double x;
			double width;
			vint height;
			vint length;
			Nullable<DocumentInlineObjectRunProperty> inlineObjectProp;
		};
		List<TempCharInfo> tempChars;
		List<Pair<vint, vint>> lineRanges; // [start, end) for each line

		double currentX = 0;
		vint currentLineStart = 0;

		for (vint i = 0; i < text.Length(); i++)
		{
			wchar_t c = text[i];
			TempCharInfo info = { i, currentX, 0, 0, 1, {} };

			// Handle \r - zero width, no line break
			if (c == L'\r')
			{
				tempChars.Add(info);
				continue;
			}

			// Get character properties
			Nullable<Pair<vint, DocumentInlineObjectRunProperty>> inlinePair;
			vint fontSize = GetFontSizeForPosition(state, i, inlinePair);

			if (inlinePair)
			{
				auto& prop = inlinePair.Value().value;
				info.width = (double)prop.size.x;
				info.height = prop.size.y;
				info.length = inlinePair.Value().key;
				info.inlineObjectProp = inlinePair.Value().value;
			}
			else
			{
				if (fontSize <= 0) fontSize = DefaultFontSize;
				info.width = GetCharacterWidth(c, fontSize);
				info.height = fontSize;
			}

			// Handle \n - always break line
			if (c == L'\n')
			{
				info.width = 0;
				tempChars.Add(info);
				lineRanges.Add({ currentLineStart, i + 1 });
				currentLineStart = i + 1;
				currentX = 0;
				continue;
			}

			// Check word wrap
			if (state.wrapLine && state.maxWidth > 0 && currentX > 0)
			{
				if (currentX + info.width > state.maxWidth)
				{
					lineRanges.Add({ currentLineStart, i });
					currentLineStart = i;
					currentX = 0;
				}
			}

			info.x = currentX;
			tempChars.Add(info);
			currentX += info.width;

			if(inlinePair)
			{
				i += inlinePair.Value().key - 1;
			}
		}

		// Add final line
		if (currentLineStart <= text.Length())
		{
			lineRanges.Add({ currentLineStart, text.Length() });
		}

		// Handle empty case
		if (lineRanges.Count() == 0)
		{
			lineRanges.Add({ 0, 0 });
		}

		// Second pass: calculate line heights using baseline alignment
		vint currentY = 0;
		for (auto [lineStart, lineEnd] : lineRanges)
		{
			vint maxAboveBaseline = 0;
			vint maxBelowBaseline = 0;

			for (auto&& info : tempChars)
			{
				if (info.caret < lineStart || info.caret >= lineEnd) continue;
				if (info.inlineObjectProp)
				{
					auto&& prop = info.inlineObjectProp.Value();
					vint baseline = prop.baseline;
					if (baseline == -1)
						baseline = info.height;
					vint above = baseline;
					vint below = info.height - baseline;
					if (above < 0) above = 0;
					if (below < 0) below = 0;
					if (maxAboveBaseline < above)
						maxAboveBaseline = above;
					if (maxBelowBaseline < below)
						maxBelowBaseline = below;
				}
				else
				{
					if (maxAboveBaseline < info.height)
						maxAboveBaseline = info.height;
				}
			}

			DocumentParagraphLineInfo line;
			line.startPos = lineStart;
			line.endPos = lineEnd;
			line.y = currentY;
			line.height = maxAboveBaseline + maxBelowBaseline + 4;
			line.baseline = maxAboveBaseline;

			// Calculate line width
			double lineWidth = 0;
			for (auto&& info : tempChars)
			{
				if (info.caret < lineStart || info.caret >= lineEnd) continue;
				double endX = info.x + info.width;
				if (endX > lineWidth) lineWidth = endX;
			}
			line.width = (vint)lineWidth;

			// Fill inline object bounds

			for (auto&& info : tempChars)
			{
				if (info.caret < lineStart || info.caret >= lineEnd) continue;
				if (info.inlineObjectProp)
				{
					auto&& prop = info.inlineObjectProp.Value();
					if (prop.callbackId != -1)
					{
						vint baseline = prop.baseline;
						if (baseline == -1)
							baseline = info.height;
						vint y = line.y + 2 + line.baseline - baseline;
						state.cachedInlineObjectBounds.Add(prop.callbackId, Rect(Point((vint)info.x, y), prop.size));
					}
				}
			}

			state.lines.Add(line);
			currentY += line.height;
		}

		// Third pass: create final character layouts with line indices
		vint lineIdx = 0;
		for (auto&& info : tempChars)
		{
			while (lineIdx < state.lines.Count() - 1 && info.caret >= state.lines[lineIdx].endPos)
			{
				lineIdx++;
			}
			DocumentParagraphCharLayout cl;
			cl.x = info.x;
			cl.width = info.width;
			cl.lineIndex = lineIdx;
			cl.height = info.height;
			cl.length = info.length;
			cl.baseline = 0;
			cl.isInlineObject = (bool)info.inlineObjectProp;
			AddCaretLayout(state, info.caret, cl);
		}

		// Calculate total size
		vint maxWidth = 0;
		for (auto&& line : state.lines)
		{
			if (line.width > maxWidth) maxWidth = line.width;
		}
		state.cachedSize = Size(maxWidth, currentY > 0 ? currentY : DefaultLineHeight);
	}

/***********************************************************************
IGuiRemoteProtocolMessages (Elements - Document)
***********************************************************************/

	void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_DocumentParagraph(vint id, const ElementDesc_DocumentParagraph& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_DocumentParagraph(vint, const ElementDesc_DocumentParagraph&)#"
		vint index = paragraphStates.Keys().IndexOf(arguments.id);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Paragraph not created.");
		auto state = paragraphStates.Values()[index];

		// Apply text if provided (distinguish null vs empty string)
		if (arguments.text)
		{
			state->text = arguments.text.Value();
			// Text changed - clear run maps since positions may be invalid
			state->textRuns.Clear();
			state->inlineObjectRuns.Clear();
		}

		// Always update these
		state->wrapLine = arguments.wrapLine;
		state->maxWidth = arguments.maxWidth;
		state->alignment = arguments.alignment;

		// Process removed inline objects first
		if (arguments.removedInlineObjects)
		{
			for (auto callbackId : *arguments.removedInlineObjects.Obj())
			{
				// Find the range for this inline object and reset it
				for (auto [range, prop] : state->inlineObjectRuns)
				{
					if (prop.callbackId == callbackId)
					{
						elements::ResetInlineObjectRun(state->inlineObjectRuns, range);
						break;
					}
				}
			}
		}

		// Apply runsDiff using helper functions
		if (arguments.runsDiff)
		{
			for (auto run : *arguments.runsDiff.Obj())
			{
				elements::CaretRange range{ run.caretBegin, run.caretEnd };

				if (auto textProp = run.props.TryGet<DocumentTextRunProperty>())
				{
					elements::DocumentTextRunPropertyOverrides overrides;
					overrides.textColor = textProp->textColor;
					overrides.backgroundColor = textProp->backgroundColor;
					overrides.fontFamily = textProp->fontProperties.fontFamily;
					overrides.size = textProp->fontProperties.size;
					// Convert bool flags back to TextStyle
					elements::IGuiGraphicsParagraph::TextStyle style = (elements::IGuiGraphicsParagraph::TextStyle)0;
					if (textProp->fontProperties.bold) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Bold);
					if (textProp->fontProperties.italic) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Italic);
					if (textProp->fontProperties.underline) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Underline);
					if (textProp->fontProperties.strikeline) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Strikeline);
					overrides.textStyle = style;
					elements::AddTextRun(state->textRuns, range, overrides);
				}
				else if (auto inlineProp = run.props.TryGet<DocumentInlineObjectRunProperty>())
				{
					bool result = elements::AddInlineObjectRun(state->inlineObjectRuns, range, *inlineProp);
					TEST_ASSERT(result);
				}
			}
		}

		// Merge runs to create final result
		state->mergedRuns.Clear();
		elements::MergeRuns(state->textRuns, state->inlineObjectRuns, state->mergedRuns);

		// Recalculate layout
		CalculateParagraphLayout(*state.Obj());

		{
			index = loggedTrace.createdElements->Keys().IndexOf(arguments.id);
			CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");

			auto rendererType = loggedTrace.createdElements->Values()[index];
			CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");

			index = lastElementDescs.Keys().IndexOf(arguments.id);
			ElementDesc_DocumentParagraphFull element;
			if (index != -1)
			{
				auto paragraphRef = lastElementDescs.Values()[index].TryGet<ElementDesc_DocumentParagraphFull>();
				CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
				element = *paragraphRef;
			}
			element.paragraph = arguments;
			element.paragraph.text = state->text;
			element.paragraph.createdInlineObjects = {};
			element.paragraph.removedInlineObjects = {};
			element.paragraph.runsDiff = Ptr(new List<remoteprotocol::DocumentRun>);
			for (auto [range, props] : state->mergedRuns)
			{
				remoteprotocol::DocumentRun run;
				run.caretBegin = range.caretBegin;
				run.caretEnd = range.caretEnd;
				run.props = props;
				element.paragraph.runsDiff->Add(run);
			}
			lastElementDescs.Set(arguments.id, element);
		}

		// Send response with calculated size and inline object bounds
		UpdateElement_DocumentParagraphResponse response;
		response.documentSize = state->cachedSize;
		GetEvents()->RespondRendererUpdateElement_DocumentParagraph(id, response);

		// Store inlineObjectBounds
		if (state->cachedInlineObjectBounds.Count() > 0)
		{
			if (!measuringForNextRendering.inlineObjectBounds)
			{
				measuringForNextRendering.inlineObjectBounds = Ptr(new List<ElementMeasuring_InlineObjectBounds>);
			}
			for (vint i = 0; i < state->cachedInlineObjectBounds.Count(); i++)
			{
				ElementMeasuring_InlineObjectBounds bounds;
				bounds.elementId = arguments.id;
				bounds.callbackId = state->cachedInlineObjectBounds.Keys()[i];
				bounds.bounds = state->cachedInlineObjectBounds.Values()[i];
				measuringForNextRendering.inlineObjectBounds.Obj()->Add(bounds);
			}
		}
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaretBounds(vint id, const GetCaretBoundsRequest& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaretBounds(vint, const GetCaretBoundsRequest&)#"
		vint index = paragraphStates.Keys().IndexOf(arguments.id);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph.");
		auto state = paragraphStates.Values()[index];

		GetCaretBoundsResponse response;
		response.frontSideBounds = Ptr(new List<Rect>);
		response.backSideBounds = Ptr(new List<Rect>);

		if (state->text.Length() == 0)
		{
			Rect bounds = { 0,0,0,DefaultLineHeight };
			response.frontSideBounds->Add(bounds);
			response.backSideBounds->Add(bounds);
			GetEvents()->RespondDocumentParagraph_GetCaretBounds(id, response);
			return;
		}

		for (vint caret = 0; caret <= state->text.Length(); caret++)
		{
			vint anchorCaret = FindLayoutCaretLE(*state.Obj(), caret);
			if (anchorCaret == -1) anchorCaret = FindLayoutCaretGT(*state.Obj(), caret);
			if (anchorCaret == -1)
			{
				Rect bounds = { 0,0,0,DefaultLineHeight };
				response.frontSideBounds->Add(bounds);
				response.backSideBounds->Add(bounds);
				continue;
			}

			auto anchorLayout = state->caretLayouts[anchorCaret];
			vint lineIndex = anchorLayout.lineIndex;
			vint y1 = state->lines[lineIndex].y;
			vint y2 = y1 + state->lines[lineIndex].height;
			vint x = (vint)anchorLayout.x;

			if (caret >= anchorCaret + anchorLayout.length)
			{
				x = (vint)(anchorLayout.x + anchorLayout.width);
			}

			response.backSideBounds->Add(Rect(x, y1, x, y2));
			response.frontSideBounds->Add(Rect(x, y1, x, y2));
		}
		GetEvents()->RespondDocumentParagraph_GetCaretBounds(id, response);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaret(vint id, const GetCaretRequest& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaret(vint, const GetCaretRequest&)#"
		vint index = paragraphStates.Keys().IndexOf(arguments.id);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph.");
		auto state = paragraphStates.Values()[index];

		vint caret = arguments.caret;
		auto relPos = arguments.relativePosition;
		GetCaretResponse response;
		response.preferFrontSide = true;

		vint textLen = state->text.Length();

		// Clamp caret
		if (caret < 0) caret = 0;
		if (caret > textLen) caret = textLen;

		// Find current line
		vint lineIdx = 0;
		for (vint i = 0; i < state->lines.Count(); i++)
		{
			if (caret >= state->lines[i].startPos && caret <= state->lines[i].endPos)
			{
				lineIdx = i;
				break;
			}
		}

		using CRP = elements::IGuiGraphicsParagraph::CaretRelativePosition;

		switch (relPos)
		{
		case CRP::CaretFirst:
			response.newCaret = 0;
			break;
		case CRP::CaretLast:
			response.newCaret = textLen;
			break;
		case CRP::CaretLineFirst:
			response.newCaret = state->lines[lineIdx].startPos;
			if (!IsValidCaretPosition(*state.Obj(), response.newCaret))
			{
				auto candidate = FindLayoutCaretGT(*state.Obj(), response.newCaret);
				if (candidate != -1 && candidate < state->lines[lineIdx].endPos)
				{
					response.newCaret = candidate;
				}
			}
			break;
		case CRP::CaretLineLast:
			response.newCaret = state->lines[lineIdx].endPos;
			if (response.newCaret > state->lines[lineIdx].startPos && response.newCaret > 0)
			{
				// Don't include CR/LF at end of line
				while (response.newCaret > state->lines[lineIdx].startPos && response.newCaret > 0)
				{
					auto ch = state->text[response.newCaret - 1];
					if (ch == L'\r' || ch == L'\n')
					{
						response.newCaret--;
					}
					else
					{
						break;
					}
				}
			}
			if (!IsValidCaretPosition(*state.Obj(), response.newCaret))
			{
				auto candidate = FindLayoutCaretLE(*state.Obj(), response.newCaret);
				if (candidate != -1 && candidate >= state->lines[lineIdx].startPos)
				{
					response.newCaret = candidate;
				}
			}
			break;
		case CRP::CaretMoveLeft:
			if (caret > 0)
			{
				elements::CaretRange inlineRange;
				if (TryGetInlineObjectRangeAtEnd(*state.Obj(), caret, inlineRange))
				{
					response.newCaret = inlineRange.caretBegin;
				}
				else if (TryGetInlineObjectRangeContaining(*state.Obj(), caret - 1, inlineRange))
				{
					response.newCaret = inlineRange.caretBegin;
				}
				else
				{
					response.newCaret = caret - 1;
				}
			}
			else
			{
				response.newCaret = 0;
			}
			break;
		case CRP::CaretMoveRight:
			if (caret < textLen)
			{
				elements::CaretRange inlineRange;
				if (TryGetInlineObjectRangeAtBegin(*state.Obj(), caret, inlineRange))
				{
					response.newCaret = inlineRange.caretEnd;
				}
				else if (TryGetInlineObjectRangeContaining(*state.Obj(), caret + 1, inlineRange))
				{
					response.newCaret = inlineRange.caretEnd;
				}
				else
				{
					response.newCaret = caret + 1;
				}
			}
			else
			{
				response.newCaret = textLen;
			}
			break;
		case CRP::CaretMoveUp:
			if (lineIdx > 0)
			{
				// Calculate x offset in current line
				vint xOffset = 0;
				if (caret > 0)
				{
					DocumentParagraphCharLayout prevLayout;
					auto layoutCaret = FindLayoutCaretLE(*state.Obj(), caret - 1);
					if (layoutCaret != -1 && TryGetLayoutAtCaret(*state.Obj(), layoutCaret, prevLayout))
					{
						xOffset = (vint)(prevLayout.x + prevLayout.width);
					}
				}
				// Find corresponding position in previous line
				auto& prevLine = state->lines[lineIdx - 1];
				response.newCaret = prevLine.startPos;
				auto keyIndex = FindFirstCaretKeyAtOrAfter(*state.Obj(), prevLine.startPos);
				if (keyIndex != -1)
				{
					for (vint i = keyIndex; i < state->caretLayoutKeys.Count(); i++)
					{
						auto caretKey = state->caretLayoutKeys[i];
						if (caretKey >= prevLine.endPos) break;
						auto& ch = state->caretLayouts[caretKey];
						if (ch.x + ch.width / 2 > xOffset)
							break;
						response.newCaret = caretKey + ch.length;
					}
				}
			}
			else
			{
				response.newCaret = caret;
			}
			break;
		case CRP::CaretMoveDown:
			if (lineIdx < state->lines.Count() - 1)
			{
				// Calculate x offset in current line
				vint xOffset = 0;
				if (caret > 0)
				{
					DocumentParagraphCharLayout prevLayout;
					auto layoutCaret = FindLayoutCaretLE(*state.Obj(), caret - 1);
					if (layoutCaret != -1 && TryGetLayoutAtCaret(*state.Obj(), layoutCaret, prevLayout))
					{
						xOffset = (vint)(prevLayout.x + prevLayout.width);
					}
				}
				// Find corresponding position in next line
				auto& nextLine = state->lines[lineIdx + 1];
				response.newCaret = nextLine.startPos;
				auto keyIndex = FindFirstCaretKeyAtOrAfter(*state.Obj(), nextLine.startPos);
				if (keyIndex != -1)
				{
					for (vint i = keyIndex; i < state->caretLayoutKeys.Count(); i++)
					{
						auto caretKey = state->caretLayoutKeys[i];
						if (caretKey >= nextLine.endPos) break;
						auto& ch = state->caretLayouts[caretKey];
						if (ch.x + ch.width / 2 > xOffset)
							break;
						response.newCaret = caretKey + ch.length;
					}
				}
			}
			else
			{
				response.newCaret = caret;
			}
			break;
		default:
			response.newCaret = caret;
			break;
		}
		while (!IsValidCaretPosition(*state.Obj(), response.newCaret))
		{
			if (response.newCaret < caret)
			{
				auto candidate = FindLayoutCaretLE(*state.Obj(), response.newCaret);
				if (candidate == -1) break;
				if (candidate == response.newCaret) break;
				response.newCaret = candidate;
			}
			else if (response.newCaret > caret)
			{
				auto candidate = FindLayoutCaretGT(*state.Obj(), response.newCaret);
				if (candidate == -1) break;
				if (candidate == response.newCaret) break;
				response.newCaret = candidate;
			}
			else
			{
				break;
			}
		}
		if (!IsValidCaretPosition(*state.Obj(), response.newCaret))
		{
			response.newCaret = caret;
		}

		GetEvents()->RespondDocumentParagraph_GetCaret(id, response);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetNearestCaretFromTextPos(vint id, const GetNearestCaretFromTextPosRequest& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetNearestCaretFromTextPos(vint, const GetCaretBoundsRequest&)#"
		vint index = paragraphStates.Keys().IndexOf(arguments.id);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph.");
		auto state = paragraphStates.Values()[index];

		vint textPos = arguments.textPos;
		vint textLen = state->text.Length();

		// Clamp to valid range
		if (textPos < 0) textPos = 0;
		if (textPos > textLen) textPos = textLen;

		if (IsValidCaretPosition(*state.Obj(), textPos))
		{
			GetEvents()->RespondDocumentParagraph_GetNearestCaretFromTextPos(id, textPos);
			return;
		}

		vint caret = -1;
		if (arguments.frontSide && textPos == textLen)
		{
			caret = textLen;
		}
		else if (arguments.frontSide)
		{
			caret = FindLayoutCaretLE(*state.Obj(), textPos);
			if (caret == -1)
			{
				caret = FindLayoutCaretGT(*state.Obj(), textPos);
			}
		}
		else
		{
			caret = FindLayoutCaretGT(*state.Obj(), textPos);
			if (caret == -1)
			{
				caret = FindLayoutCaretLE(*state.Obj(), textPos);
			}
		}
		if (caret == -1) caret = 0;
		GetEvents()->RespondDocumentParagraph_GetNearestCaretFromTextPos(id, caret);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetInlineObjectFromPoint(vint id, const GetInlineObjectFromPointRequest& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetInlineObjectFromPoint(vint, const GetInlineObjectFromPointRequest&)#"
		vint index = paragraphStates.Keys().IndexOf(arguments.id);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph.");
		auto state = paragraphStates.Values()[index];

		Point pt = arguments.point;
		Nullable<remoteprotocol::DocumentRun> result;

		// Find the line containing the Y coordinate
		vint lineIdx = -1;
		for (vint i = 0; i < state->lines.Count(); i++)
		{
			if (pt.y >= state->lines[i].y && pt.y < state->lines[i].y + state->lines[i].height)
			{
				lineIdx = i;
				break;
			}
		}

		if (lineIdx >= 0)
		{
			auto& line = state->lines[lineIdx];

			// Apply alignment offset
			vint alignmentOffset = 0;
			if (state->alignment == ElementHorizontalAlignment::Center)
			{
				alignmentOffset = (state->cachedSize.x - line.width) / 2;
			}
			else if (state->alignment == ElementHorizontalAlignment::Right)
			{
				alignmentOffset = state->cachedSize.x - line.width;
			}
			vint relativeX = pt.x - alignmentOffset;

			// Check each character in the line
			auto keyIndex = FindFirstCaretKeyAtOrAfter(*state.Obj(), line.startPos);
			if (keyIndex != -1)
			{
				for (vint i = keyIndex; i < state->caretLayoutKeys.Count(); i++)
				{
					auto caretKey = state->caretLayoutKeys[i];
					if (caretKey >= line.endPos) break;
					auto& ch = state->caretLayouts[caretKey];
					if (ch.isInlineObject && relativeX >= ch.x && relativeX < ch.x + ch.width)
					{
						// Found an inline object - look up its properties
						for (auto [range, prop] : state->mergedRuns)
						{
							if (caretKey >= range.caretBegin && caretKey < range.caretEnd)
							{
								if (auto inlineProp = prop.TryGet<DocumentInlineObjectRunProperty>())
								{
									remoteprotocol::DocumentRun run;
									run.caretBegin = range.caretBegin;
									run.caretEnd = range.caretEnd;
									run.props = *inlineProp;
									result = run;
									break;
								}
							}
						}
						break;
					}
				}
			}
		}

		GetEvents()->RespondDocumentParagraph_GetInlineObjectFromPoint(id, result);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_IsValidCaret(vint id, const IsValidCaretRequest& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_IsValidCaret(vint, const IsValidCaretRequest&)#"
		vint index = paragraphStates.Keys().IndexOf(arguments.id);
		if (index == -1)
		{
			GetEvents()->RespondDocumentParagraph_IsValidCaret(id, false);
			return;
		}
		auto state = paragraphStates.Values()[index];

		vint caret = arguments.caret;

		GetEvents()->RespondDocumentParagraph_IsValidCaret(id, IsValidCaretPosition(*state.Obj(), caret));
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_OpenCaret(const OpenCaretRequest& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_OpenCaret(const OpenCaretRequest&)#"
		vint index = loggedTrace.createdElements->Keys().IndexOf(arguments.id);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");

		auto rendererType = loggedTrace.createdElements->Values()[index];
		CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");

		index = lastElementDescs.Keys().IndexOf(arguments.id);
		ElementDesc_DocumentParagraphFull element;
		if (index != -1)
		{
			auto paragraphRef = lastElementDescs.Values()[index].TryGet<ElementDesc_DocumentParagraphFull>();
			CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
			element = *paragraphRef;
		}
		element.caret = arguments;
		lastElementDescs.Set(arguments.id, element);
#undef ERROR_MESSAGE_PREFIX
	}

	void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_CloseCaret(const vint& arguments)
	{
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_CloseCaret(const vint&)#"
		vint index = loggedTrace.createdElements->Keys().IndexOf(arguments);
		CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");

		auto rendererType = loggedTrace.createdElements->Values()[index];
		CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");

		index = lastElementDescs.Keys().IndexOf(arguments);
		ElementDesc_DocumentParagraphFull element;
		if (index != -1)
		{
			auto paragraphRef = lastElementDescs.Values()[index].TryGet<ElementDesc_DocumentParagraphFull>();
			CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
			element = *paragraphRef;
		}
		element.caret.Reset();
		lastElementDescs.Set(arguments, element);
#undef ERROR_MESSAGE_PREFIX
	}
}


/***********************************************************************
.\GUIUNITTESTPROTOCOL_SHARED.CPP
***********************************************************************/

namespace vl::presentation::unittest
{
	using namespace vl::collections;

/***********************************************************************
UnitTestScreenConfig
***********************************************************************/

	void UnitTestScreenConfig::FastInitialize(vint width, vint height, vint taskBarHeight)
	{
		executablePath = WString::Unmanaged(L"/GacUI/Remote/Protocol/UnitTest.exe");
		customFramePadding = { 8,8,8,8 };

		fontConfig.defaultFont.fontFamily = WString::Unmanaged(L"GacUI Default Font");
		fontConfig.defaultFont.size = 12;
		fontConfig.supportedFonts = Ptr(new List<WString>());
		fontConfig.supportedFonts->Add(fontConfig.defaultFont.fontFamily);

		screenConfig.bounds = { 0,0,width,height };
		screenConfig.clientBounds = { 0,0,width,(height - taskBarHeight) };
		screenConfig.scalingX = 1;
		screenConfig.scalingY = 1;
	}
}

/***********************************************************************
.\GUIUNITTESTUTILITIES.CPP
***********************************************************************/

namespace vl::presentation::controls
{
	extern bool GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES;
	extern bool GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD;
}

namespace vl::presentation::unittest
{
	const UnitTestFrameworkConfig*			globalUnitTestFrameworkConfig = nullptr;

	const UnitTestFrameworkConfig& GetUnitTestFrameworkConfig()
	{
		CHECK_ERROR(globalUnitTestFrameworkConfig, L"vl::presentation::unittest::GetUnitTestFrameworkConfig()#GacUIUnitTest_Initialize has not been called.");
		return *globalUnitTestFrameworkConfig;
	}
}

namespace vl::presentation::GuiHostedController_UnitTestHelper
{
	extern bool ExceptionOccuredUnderUnitTestReleaseMode();
}

using namespace vl;
using namespace vl::collections;
using namespace vl::filesystem;
using namespace vl::reflection::description;
using namespace vl::glr::json;
using namespace vl::presentation;
using namespace vl::presentation::remoteprotocol;
using namespace vl::presentation::controls;
using namespace vl::presentation::unittest;
using namespace vl::presentation::GuiHostedController_UnitTestHelper;

class UnitTestContextImpl : public Object, public virtual IUnitTestContext
{
	UnitTestRemoteProtocol*		protocol = nullptr;

public:
	UnitTestContextImpl(UnitTestRemoteProtocol* _protocol)
		: protocol(_protocol)
	{
	}

	UnitTestRemoteProtocol* GetProtocol()
	{
		return protocol;
	}
};

UnitTestMainFunc guiMainProxy;
UnitTestContextImpl* guiMainUnitTestContext = nullptr;

void GacUIUnitTest_Initialize(const UnitTestFrameworkConfig* config)
{
	CHECK_ERROR(config, L"GacUIUnitTest_Initialize()#Argument config should not be null.");
	globalUnitTestFrameworkConfig = config;

	GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES = true;
	GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD = true;

	GetGlobalTypeManager()->Load();
	GetPluginManager()->Load(true, false);
}

void GacUIUnitTest_Finalize()
{
	ResetGlobalTypeManager();
	GetPluginManager()->Unload(true, false);
	DestroyPluginManager();
	ThreadLocalStorage::DisposeStorages();

	GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES = false;
	GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD = false;
	globalUnitTestFrameworkConfig = nullptr;
}

void GacUIUnitTest_SetGuiMainProxy(const UnitTestMainFunc& proxy)
{
	guiMainProxy = proxy;
}

void GacUIUnitTest_LinkGuiMainProxy(const UnitTestLinkFunc& proxy)
{
	auto previousMainProxy = guiMainProxy;
	GacUIUnitTest_SetGuiMainProxy([=](UnitTestRemoteProtocol* protocol, IUnitTestContext* context)
	{
		proxy(protocol, context, previousMainProxy);
	});
}

File GacUIUnitTest_PrepareSnapshotFile(const WString& appName, const WString& extension)
{
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_PrepareSnapshotFile(const WString&, const WString&)#"
	Folder snapshotFolder = GetUnitTestFrameworkConfig().snapshotFolder;
	CHECK_ERROR(snapshotFolder.Exists(), ERROR_MESSAGE_PREFIX L"UnitTestFrameworkConfig::snapshotFolder does not point to an existing folder.");

	File snapshotFile = snapshotFolder.GetFilePath() / (appName + extension);
	{
		auto pathPrefix = snapshotFolder.GetFilePath().GetFullPath() + WString::FromChar(FilePath::GetPathDelimiter());
		auto snapshotPath = snapshotFile.GetFilePath().GetFullPath();
		CHECK_ERROR(
			snapshotPath.Length() > pathPrefix.Length() && snapshotPath.Left(pathPrefix.Length()) == pathPrefix,
			ERROR_MESSAGE_PREFIX L"Argument appName should specify a file that is inside UnitTestFrameworkConfig::snapshotFolder"
			);
		Folder snapshotFileFolder = snapshotFile.GetFilePath().GetFolder();
		if (!snapshotFileFolder.Exists())
		{
			CHECK_ERROR(snapshotFileFolder.Create(true), ERROR_MESSAGE_PREFIX L"Failed to create the folder to contain the snapshot file specified by argument appName.");
		}
	}

	return snapshotFile;
#undef ERROR_MESSAGE_PREFIX
}

Folder GacUIUnitTest_PrepareSnapshotFramesFolder(const WString& appName)
{
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_PrepareSnapshotFile(const WString&, const WString&)#"
	Folder snapshotRootFolder = GetUnitTestFrameworkConfig().snapshotFolder;
	CHECK_ERROR(snapshotRootFolder.Exists(), ERROR_MESSAGE_PREFIX L"UnitTestFrameworkConfig::snapshotFolder does not point to an existing folder.");

	Folder snapshotFramesFolder = snapshotRootFolder.GetFilePath() / appName;
	if (!snapshotFramesFolder.Exists())
	{
		CHECK_ERROR(snapshotFramesFolder.Create(true), ERROR_MESSAGE_PREFIX L"Failed to create the folder containing frame snapshots.");
	}

	return snapshotFramesFolder;
#undef ERROR_MESSAGE_PREFIX
}

void GacUIUnitTest_WriteSnapshotFileIfChanged(File& snapshotFile, const WString& textLog)
{
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_WriteSnapshotFileIfChanged(File&, const WString&)#"
	bool skipWriting = false;
	if (snapshotFile.Exists())
	{
		auto previousLog = snapshotFile.ReadAllTextByBom();
		if (previousLog == textLog)
		{
			skipWriting = true;
		}
	}
	if (!skipWriting)
	{
		bool succeeded = snapshotFile.WriteAllText(textLog, true, stream::BomEncoder::Utf8);
		CHECK_ERROR(succeeded, ERROR_MESSAGE_PREFIX L"Failed to write the snapshot file.");
	}
#undef ERROR_MESSAGE_PREFIX
}

void GacUIUnitTest_LogUI(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol)
{
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_LogUI(const WString&, UnitTestRemoteProtocol&)#"
	File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L".json"));
	Folder snapshotFramesFolder = GacUIUnitTest_PrepareSnapshotFramesFolder(appName);

	JsonFormatting formatting;
	formatting.spaceAfterColon = true;
	formatting.spaceAfterComma = true;
	formatting.crlf = true;
	formatting.compact = true;

	auto renderingTrace = unitTestProtocol.GetLoggedTrace();
	List<WString> frameTextLogs;
	SortedList<WString> frameFileNames;
	remoteprotocol::UnitTest_RenderingTrace deserialized;

	auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(unitTestProtocol.GetLoggedTrace());
	{
		auto textLog = JsonToString(jsonLog, formatting);
		remoteprotocol::ConvertJsonToCustomType(jsonLog, deserialized);
		auto jsonLog2 = remoteprotocol::ConvertCustomTypeToJson(deserialized);
		auto textLog2 = JsonToString(jsonLog2, formatting);
		CHECK_ERROR(textLog == textLog2, ERROR_MESSAGE_PREFIX L"Serialization and deserialization doesn't match.");
	}

	if (renderingTrace.frames)
	{
		for (vint i = 0; i < renderingTrace.frames->Count(); i++)
		{
			auto&& frame = renderingTrace.frames->Get(i);
			jsonLog = remoteprotocol::ConvertCustomTypeToJson(frame);
			auto textLog = JsonToString(jsonLog, formatting);

			WString frameFileName = L"frame_" + itow(i) + L".json";
			frameFileNames.Add(frameFileName);
			File snapshotFrameFile = snapshotFramesFolder.GetFilePath() / frameFileName;
			GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFrameFile, textLog);

			renderingTrace.frames->Set(i, { .frameId = frame.frameId, .frameName = frame.frameName });
		}
	}

	{
		jsonLog = remoteprotocol::ConvertCustomTypeToJson(unitTestProtocol.GetLoggedTrace());
		auto textLog = JsonToString(jsonLog, formatting);
		GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog);
	}

	List<File> existingFrameFiles;
	snapshotFramesFolder.GetFiles(existingFrameFiles);
	for (auto&& file : existingFrameFiles)
	{
		if (!frameFileNames.Contains(file.GetFilePath().GetName()))
		{
			CHECK_ERROR(file.Delete(), ERROR_MESSAGE_PREFIX L"Failed to delete unnecessary frame file.");
		}
	}
#undef ERROR_MESSAGE_PREFIX
}

void GacUIUnitTest_LogCommands(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol)
{
	File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L"[commands].txt"));

	JsonFormatting formatting;
	formatting.spaceAfterColon = true;
	formatting.spaceAfterComma = true;
	formatting.crlf = false;
	formatting.compact = true;

	auto textLog = stream::GenerateToStream([&unitTestProtocol, &formatting](stream::TextWriter& writer)
	{
		auto&& loggedFrames = unitTestProtocol.GetLoggedFrames();
		for (auto loggedFrame : loggedFrames)
		{
			writer.WriteLine(L"========================================");
			writer.WriteLine(itow(loggedFrame->frameId));
			writer.WriteLine(L"========================================");
			for (auto&& commandLog : loggedFrame->renderingCommandsLog)
			{
				writer.WriteLine(commandLog);
			}
		};
	});

	GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog);
}

void GacUIUnitTest_LogDiffs(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol)
{
	File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L"[diffs].txt"));

	JsonFormatting formatting;
	formatting.spaceAfterColon = true;
	formatting.spaceAfterComma = true;
	formatting.crlf = false;
	formatting.compact = true;

	auto textLog = stream::GenerateToStream([&unitTestProtocol, &formatting](stream::TextWriter& writer)
	{
		Ptr<RenderingDom> dom;
		DomIndex domIndex;
		auto&& loggedFrames = unitTestProtocol.GetLoggedFrames();
		for (auto loggedFrame : loggedFrames)
		{
			writer.WriteLine(L"========================================");
			writer.WriteLine(itow(loggedFrame->frameId));
			writer.WriteLine(L"========================================");

			if (!dom)
			{
				dom = loggedFrame->renderingDom;
				BuildDomIndex(dom, domIndex);

				List<Pair<vint, Ptr<RenderingDom>>> lines;
				lines.Add({ 0,dom });
				for (vint i = 0; i < lines.Count(); i++)
				{
					for (vint j = 0; j < lines[i].key; j++)
					{
						writer.WriteString(L"  ");
					}

					auto line = lines[i].value;
					writer.WriteString(itow(line->id));
					writer.WriteString(L": ");

					auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(line->content);
					writer.WriteLine(JsonToString(jsonLog, formatting));

					if (line->children)
					{
						for (auto child : *line->children.Obj())
						{
							lines.Add({ lines[i].key + 1,child });
						}
					}
				}
			}
			else
			{
				DomIndex nextDomIndex;
				BuildDomIndex(loggedFrame->renderingDom, nextDomIndex);

				Ptr<List<RenderingDom_Diff>> diffList;
				if (loggedFrame->renderingDiffs)
				{
					diffList = loggedFrame->renderingDiffs.Value().diffsInOrder;
				}
				else
				{
					RenderingDom_DiffsInOrder diffs;
					DiffDom(dom, domIndex, loggedFrame->renderingDom, nextDomIndex, diffs);
					diffList = diffs.diffsInOrder;

					auto copiedDom = CopyDom(dom);
					DomIndex copiedDomIndex;
					BuildDomIndex(copiedDom, copiedDomIndex);
					UpdateDomInplace(copiedDom, copiedDomIndex, diffs);

					auto expectedJson = JsonToString(remoteprotocol::ConvertCustomTypeToJson(loggedFrame->renderingDom));
					auto actualJson = JsonToString(remoteprotocol::ConvertCustomTypeToJson(copiedDom));
					TEST_ASSERT(actualJson == expectedJson);
				}

				if (diffList)
				{
					for (auto&& diff : *diffList.Obj())
					{
						auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(diff);
						writer.WriteLine(JsonToString(jsonLog, formatting));
					}
				}

				dom = loggedFrame->renderingDom;
				domIndex = std::move(nextDomIndex);
			}
		};
	});

	GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog);
}

namespace vl::presentation::remoteprotocol::channeling
{
	class UnitTestChannelServer
		: public Object
		, public virtual inter_process::INetworkProtocolServer
	{
	private:
		bool stopped = true;

	public:
		inter_process::WaitForClientResult OnClientConnected(inter_process::INetworkProtocolConnection* connection) override
		{
			return inter_process::WaitForClientResult::Reject;
		}

		void Start() override
		{
			stopped = false;
		}

		void Stop() override
		{
			stopped = true;
		}

		bool IsStopped() override
		{
			return stopped;
		}
	};

	using GuiRemoteProtocolChannelServer = GuiRemoteProtocolNetworkChannelServer<UnitTestChannelServer>;
}

void GacUIUnitTest_Start(const WString& appName, Nullable<UnitTestScreenConfig> config)
{
	UnitTestScreenConfig globalConfig;
	if (config)
	{
		globalConfig = config.Value();
	}
	else
	{
		globalConfig.FastInitialize(1024, 768);
	}

	// Renderer
	UnitTestRemoteProtocol unitTestProtocol(appName, globalConfig);
	auto jsonParser = Ptr(new glr::json::Parser);

	Ptr<channeling::GuiRemoteProtocolChannelServer> channelServer;
	Ptr<channeling::GuiRemoteProtocolLocalChannelClient> coreClient, rendererClient;
	Ptr<channeling::GuiRemoteProtocolRendererChannel> rendererChannel;
	Ptr<channeling::GuiRemoteProtocolCoreChannel> coreChannel;

	IGuiRemoteProtocol* coreProtocol = unitTestProtocol.GetProtocol();
	if (globalConfig.useChannel == UnitTestRemoteChannel::Sync)
	{
		channelServer = Ptr(new channeling::GuiRemoteProtocolChannelServer(jsonParser));
		coreClient = Ptr(new channeling::GuiRemoteProtocolLocalChannelClient(jsonParser));
		rendererClient = Ptr(new channeling::GuiRemoteProtocolLocalChannelClient(jsonParser));
		channelServer->Start();
		TEST_ASSERT(channelServer->ConnectLocalClient(coreClient) > 0);
		TEST_ASSERT(channelServer->ConnectLocalClient(rendererClient) > 0);

		rendererChannel = Ptr(new channeling::GuiRemoteProtocolRendererChannel(rendererClient->GetProtocolChannel(), unitTestProtocol.GetProtocol()));
		coreChannel = Ptr(new channeling::GuiRemoteProtocolCoreChannel(coreClient.Obj(), coreClient->GetProtocolChannel(), globalConfig.executablePath, unitTestProtocol.GetRemoteEventProcessor()));
		coreProtocol = coreChannel.Obj();
	}

	// Core
	repeatfiltering::GuiRemoteProtocolFilterVerifier verifierProtocol(
		coreProtocol
		);
	repeatfiltering::GuiRemoteProtocolFilter filteredProtocol(&verifierProtocol);
	GuiRemoteProtocolDomDiffConverter diffConverterProtocol(&filteredProtocol);

	UnitTestContextImpl unitTestContext(&unitTestProtocol);
	guiMainUnitTestContext = &unitTestContext;
	SetupRemoteNativeController(
		globalConfig.useDomDiff
		? static_cast<IGuiRemoteProtocol*>(&diffConverterProtocol)
		: &filteredProtocol
		);
	GacUIUnitTest_SetGuiMainProxy({});
	TEST_ASSERT(!ExceptionOccuredUnderUnitTestReleaseMode());

	GacUIUnitTest_LogUI(appName, unitTestProtocol);
	if (!globalConfig.useDomDiff)
	{
		GacUIUnitTest_LogCommands(appName, unitTestProtocol);
	}
	GacUIUnitTest_LogDiffs(appName, unitTestProtocol);
}

template<typename T>
Thread* RunInNewThread(T&& threadProc)
{
	return Thread::CreateAndStart([threadProc]()
	{
		try
		{
			threadProc();
		}
		catch (const Exception& e)
		{
			(void)e;
			throw;
		}
		catch (const Error& e)
		{
			(void)e;
			throw;
		}
	}, false);
}

void GacUIUnitTest_StartAsync(const WString& appName, Nullable<UnitTestScreenConfig> config)
{
	TEST_ASSERT(config && config.Value().useChannel == UnitTestRemoteChannel::Async);

	// Renderer
	UnitTestRemoteProtocol unitTestProtocol(appName, config.Value());
	auto jsonParser = Ptr(new glr::json::Parser);

	auto channelServer = Ptr(new channeling::GuiRemoteProtocolChannelServer(jsonParser));
	auto coreClient = Ptr(new channeling::GuiRemoteProtocolLocalChannelClient(jsonParser));
	auto rendererClient = Ptr(new channeling::GuiRemoteProtocolLocalChannelClient(jsonParser));
	channelServer->Start();
	TEST_ASSERT(channelServer->ConnectLocalClient(coreClient) > 0);
	TEST_ASSERT(channelServer->ConnectLocalClient(rendererClient) > 0);

	channeling::GuiRemoteProtocolRendererChannel rendererChannel(rendererClient->GetProtocolChannel(), unitTestProtocol.GetProtocol());
	channeling::GuiRemoteProtocolAsyncJsonChannel asyncChannelSender(coreClient->GetProtocolChannel(), unitTestProtocol.GetRemoteEventProcessor());

	EventObject eventStopped;
	TEST_ASSERT(eventStopped.CreateManualUnsignal(false));
	auto coreThread = RunInNewThread([&]()
	{
		channeling::GuiRemoteProtocolCoreChannel channelSender(coreClient.Obj(), &asyncChannelSender, config.Value().executablePath, asyncChannelSender.GetRemoteEventProcessor());

		// Core
		repeatfiltering::GuiRemoteProtocolFilterVerifier verifierProtocol(&channelSender);
		repeatfiltering::GuiRemoteProtocolFilter filteredProtocol(&verifierProtocol);
		GuiRemoteProtocolDomDiffConverter diffConverterProtocol(&filteredProtocol);

		UnitTestContextImpl unitTestContext(&unitTestProtocol);
		guiMainUnitTestContext = &unitTestContext;
		SetupRemoteNativeController(
			config.Value().useDomDiff
			? static_cast<IGuiRemoteProtocol*>(&diffConverterProtocol)
			: &filteredProtocol
			);
		GacUIUnitTest_SetGuiMainProxy({});
		eventStopped.Signal();
	});
	eventStopped.Wait();
	coreThread->Wait();
	delete coreThread;
	TEST_ASSERT(!ExceptionOccuredUnderUnitTestReleaseMode());

	GacUIUnitTest_LogUI(appName, unitTestProtocol);
	if (!config.Value().useDomDiff)
	{
		GacUIUnitTest_LogCommands(appName, unitTestProtocol);
	}
	GacUIUnitTest_LogDiffs(appName, unitTestProtocol);
}

void GacUIUnitTest_Start_WithResourceAsText(const WString& appName, Nullable<UnitTestScreenConfig> config, const WString& resourceText)
{
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_Start_WithResourceAsText(const WString&, Nullable<UnitTestScreenConfig>, const WString&)#"
	GacUIUnitTest_LinkGuiMainProxy([=](UnitTestRemoteProtocol* protocol, IUnitTestContext* context, const UnitTestMainFunc& previousMainProxy)
	{
		auto resource = GacUIUnitTest_CompileAndLoad(resourceText);
		{
			auto workflow = resource->GetStringByPath(L"UnitTest/Workflow");
			File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(
				appName,
#ifdef VCZH_64
				WString::Unmanaged(L"[x64].txt")
#else
				WString::Unmanaged(L"[x86].txt")
#endif
				);

			bool skipWriting = false;
			if (snapshotFile.Exists())
			{
				auto previousLog = snapshotFile.ReadAllTextByBom();
				if (previousLog == workflow)
				{
					skipWriting = true;
				}
			}
			if (!skipWriting)
			{
				bool succeeded = snapshotFile.WriteAllText(workflow, true, stream::BomEncoder::Utf8);
				CHECK_ERROR(succeeded, ERROR_MESSAGE_PREFIX L"Failed to write the snapshot file.");
			}
		}
		previousMainProxy(protocol, context);
	});

	if (config && config.Value().useChannel == UnitTestRemoteChannel::Async)
	{
		GacUIUnitTest_StartAsync(appName, config);
	}
	else
	{
		GacUIUnitTest_Start(appName, config);
	}
#undef ERROR_MESSAGE_PREFIX
}

void GacUIUnitTest_PrintErrors(GuiResourceError::List& errors)
{
	for (auto&& error : errors)
	{
		TEST_PRINT(L"Error in resource: " + error.location.resourcePath);
		TEST_PRINT(L"  ROW: " + itow(error.position.row) + L", COLUMN: " + itow(error.position.column));
		TEST_PRINT(L"  REASON: " + error.message);
	}
}

Ptr<GuiResource> GacUIUnitTest_CompileAndLoad(const WString& xmlResource)
{
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_CompileAndLoad(const WString&)#"
	Ptr<GuiResource> resource;
	GuiResourceError::List errors;
	{
		auto resourcePath = (GetUnitTestFrameworkConfig().resourceFolder / L"Resource.xml").GetFullPath();
		auto parser = GetParserManager()->GetParser<glr::xml::XmlDocument>(L"XML");
		auto xml = parser->Parse({ WString::Empty,resourcePath }, xmlResource, errors);
		if(!xml || errors.Count()> 0)
		{
			GacUIUnitTest_PrintErrors(errors);
			CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to parse XML resource.");
		}

		resource = GuiResource::LoadFromXml(xml, resourcePath, GetFolderPath(resourcePath), errors);
		if (!resource || errors.Count() > 0)
		{
			GacUIUnitTest_PrintErrors(errors);
			CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to load XML resource.");
		}
	}

	auto precompiledFolder = resource->Precompile(
#ifdef VCZH_64
		GuiResourceCpuArchitecture::x64,
#else
		GuiResourceCpuArchitecture::x86,
#endif
		nullptr,
		errors
		);
	if (!precompiledFolder || errors.Count() > 0)
	{
		GacUIUnitTest_PrintErrors(errors);
		CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to precompile XML resource.");
	}

	auto compiledWorkflow = precompiledFolder->GetValueByPath(WString::Unmanaged(L"Workflow/InstanceClass")).Cast<GuiInstanceCompiledWorkflow>();
	CHECK_ERROR(compiledWorkflow, ERROR_MESSAGE_PREFIX L"Failed to compile generated Workflow script.");
	CHECK_ERROR(compiledWorkflow->assembly, ERROR_MESSAGE_PREFIX L"Failed to load Workflow assembly.");

	{
		WString text;
		auto& codes = compiledWorkflow->assembly->insAfterCodegen->moduleCodes;
		for (auto [code, codeIndex] : indexed(codes))
		{
			text += L"================================(" + itow(codeIndex + 1) + L"/" + itow(codes.Count()) + L")================================\r\n";
			text += code + L"\r\n";
		}
		resource->CreateValueByPath(
			WString::Unmanaged(L"UnitTest/Workflow"),
			WString::Unmanaged(L"Text"),
			Ptr(new GuiTextData(text))
			);
	}

	GetResourceManager()->SetResource(resource, errors, GuiResourceUsage::InstanceClass);
	CHECK_ERROR(errors.Count() == 0, ERROR_MESSAGE_PREFIX L"Failed to load compiled XML resource.");

	return resource;
#undef ERROR_MESSAGE_PREFIX
}

void GuiMain()
{
	if (guiMainUnitTestContext)
	{
		guiMainProxy(guiMainUnitTestContext->GetProtocol(), guiMainUnitTestContext);
	}
	else
	{
		guiMainProxy(nullptr, nullptr);
	}
	guiMainUnitTestContext = nullptr;
}

