Effigy.Tests/PaintMaterialTests.cs
using System;
using Effigy;
using static Effigy.Tests.Report;

namespace Effigy.Tests;

/// <summary>
/// The authoring half of the paint path: what the painted canvas bakes to, and the .vmat text that
/// compiles it. A headless suite cannot compile an asset, but it can say whether the file the editor
/// will write is structurally sound and names the right image — the same bargain VmdlMaterialsTests
/// makes for the remap node.
/// </summary>
public static class PaintMaterialTests
{
	public static void Run()
	{
		Section( "paint material: the canvas bakes opaque over the base colour" );
		TestOpaqueBake();

		Section( "paint bind: the atlas goes on the slots the faces actually wear" );
		TestBindUsesThePaintedSlots();
		TestMergeDropIsNamed();

		Section( "paint material: the vmat names its image" );
		TestVmatNamesTheImage();
	}

	static void TestOpaqueBake()
	{
		var canvas = new PaintCanvas( 2, 1 );

		// One texel painted red, one left transparent.
		canvas.Blend( 0, 0, 255, 0, 0, 1f );

		var opaque = PaintMaterial.OpaqueRgba( canvas );

		Check( "the bake is the right size", opaque.Length == 2 * 1 * 4, $"{opaque.Length}" );

		Check( "a painted texel stays its colour and goes opaque",
			opaque[0] == 255 && opaque[1] == 0 && opaque[2] == 0 && opaque[3] == 255,
			$"({opaque[0]},{opaque[1]},{opaque[2]},{opaque[3]})" );

		Check( "an unpainted texel falls back to the default base, not white",
			opaque[4] == PaintMaterial.DefaultBaseR
			&& opaque[5] == PaintMaterial.DefaultBaseG
			&& opaque[6] == PaintMaterial.DefaultBaseB
			&& opaque[7] == 255,
			$"({opaque[4]},{opaque[5]},{opaque[6]},{opaque[7]})" );

		var overRed = PaintMaterial.OpaqueRgba( canvas, 200, 10, 10 );

		Check( "a caller can pass the bound material's colour as the base",
			overRed[4] == 200 && overRed[5] == 10 && overRed[6] == 10 && overRed[7] == 255 );
	}

	static void TestBindUsesThePaintedSlots()
	{
		var mesh = Primitives.Box( 2f, 2f, 2f );

		foreach ( var face in mesh.Faces )
			face.Material = 2;

		var slots = PaintBind.SlotsToBind( mesh );

		Check( "faces on slot 2 bind slot 2, not slot 0",
			slots.Count == 1 && slots[0] == 2,
			string.Join( ",", slots ) );

		Check( "slot 0 is not invented", !slots.Contains( 0 ) );
	}

	static void TestMergeDropIsNamed()
	{
		var a = Primitives.Box( 2f, 2f, 2f );
		var b = Primitives.Box( 2f, 2f, 2f );
		a.Paint = new PaintCanvas( 8, 8 );
		b.Paint = new PaintCanvas( 8, 8 );

		Check( "two painted bodies merging is a drop, said rather than silent",
			PaintBind.MergeDropsPaint( a, b ) );

		var plain = Primitives.Box( 1f, 1f, 1f );

		Check( "a painted body into an unpainted rest is not a drop",
			!PaintBind.MergeDropsPaint( plain, a ) );
	}

	static void TestVmatNamesTheImage()
	{
		var vmat = PaintMaterial.VmatSource( "models/effigy/box_paint.png" );

		Check( "the vmat binds the PNG as its colour",
			vmat.Contains( "TextureColor \"models/effigy/box_paint.png\"" ) );

		Check( "braces balance", CountOf( vmat, "{" ) == CountOf( vmat, "}" ),
			$"{CountOf( vmat, "{" )} open, {CountOf( vmat, "}" )} close" );

		Check( "it is the Layer0 form the engine's own templates ship",
			vmat.Contains( "Layer0" ) && vmat.Contains( "shader \"complex.vfx\"" ) );

		// A .vtex is not a valid texture input — the engine's compiler says so in its own words.
		// The material must name the image, not a texture wrapper.
		Check( "and it never names a .vtex",
			!vmat.Contains( ".vtex" ) );
	}

	static int CountOf( string text, string needle )
	{
		var count = 0;
		var at = 0;

		while ( (at = text.IndexOf( needle, at, StringComparison.Ordinal )) >= 0 )
		{
			count++;
			at += needle.Length;
		}

		return count;
	}
}