UnitTests/TailBoxFileSystemTests.cs
using Sandbox.TailBox;
using System;
using System.IO;
using System.Linq;

[TestClass]
public sealed class TailBoxFileSystemTests
{
	[TestMethod]
	public void GlobMatcherHandlesDoubleStarQuestionAndBackslashes()
	{
		var razorGlob = TailBoxGlobMatcher.FromGlob( "Code/**/*.razor" );
		Assert.IsTrue( razorGlob.IsMatch( "Code/Screen.razor" ) );
		Assert.IsTrue( razorGlob.IsMatch( "Code/Ui/Nested/Screen.razor" ) );
		Assert.IsFalse( razorGlob.IsMatch( "Code/Screen.razor.scss" ) );

		var questionGlob = TailBoxGlobMatcher.FromGlob( "Ui/Panel?.razor" );
		Assert.IsTrue( questionGlob.IsMatch( "Ui/Panel1.razor" ) );
		Assert.IsFalse( questionGlob.IsMatch( "Ui/Panel10.razor" ) );

		var slashGlob = TailBoxGlobMatcher.FromGlob( "\\Code\\*.razor" );
		Assert.IsTrue( slashGlob.IsMatch( "Code\\Panel.razor" ) );
	}

	[TestMethod]
	public void TryGetRelativeProjectPathNormalizesInsidePathsAndRejectsOutsidePaths()
	{
		var root = CreateTempProject();
		try
		{
			var inside = Path.Combine( root, "Code", "Screen.razor" );
			var outside = Path.Combine( Path.GetDirectoryName( root )!, Guid.NewGuid().ToString( "N" ), "Screen.razor" );

			Assert.IsTrue( TailBoxProjectFileSystem.TryGetRelativeProjectPath( root, inside, out var relative ) );
			Assert.AreEqual( "Code/Screen.razor", relative );
			Assert.IsFalse( TailBoxProjectFileSystem.TryGetRelativeProjectPath( root, outside, out _ ) );
			Assert.IsFalse( TailBoxProjectFileSystem.TryGetRelativeProjectPath( "", inside, out _ ) );
			Assert.IsFalse( TailBoxProjectFileSystem.TryGetRelativeProjectPath( root, "", out _ ) );
		}
		finally
		{
			DeleteTempProject( root );
		}
	}

	[TestMethod]
	public void FindContentFilesHonorsGlobsAndSkipsGeneratedHiddenAndBuildFolders()
	{
		var root = CreateTempProject();
		try
		{
			WriteFile( root, "Code/Screen.razor", "<div />" );
			WriteFile( root, "Ui/Nested/Panel.razor", "<div />" );
			WriteFile( root, "Code/Screen.razor.scss", ".ignored {}" );
			WriteFile( root, "Code/tailwand.generated.scss", ".ignored {}" );
			WriteFile( root, "Code/bin/Skip.razor", "<div />" );
			WriteFile( root, "obj/Skip.razor", "<div />" );
			WriteFile( root, ".vs/Skip.razor", "<div />" );
			WriteFile( root, ".sbox/Skip.razor", "<div />" );
			WriteFile( root, ".git/Skip.razor", "<div />" );

			var config = TailBoxConfig.CreateDefault();
			config.Content.Clear();
			config.Content.Add( "Code/**/*.razor" );
			config.Content.Add( "Ui/**/*.razor" );
			var output = Path.Combine( root, "Code", "tailwand.generated.scss" );

			var files = TailBoxProjectFileSystem.FindContentFiles( root, config, output )
				.Select( file => Path.GetRelativePath( root, file ).Replace( '\\', '/' ) )
				.ToHashSet( StringComparer.Ordinal );

			CollectionAssert.AreEquivalent( new[] { "Code/Screen.razor", "Ui/Nested/Panel.razor" }, files.ToArray() );
		}
		finally
		{
			DeleteTempProject( root );
		}
	}

	[TestMethod]
	public void WriteIfChangedCreatesDirectoriesAndReportsChangeState()
	{
		var root = CreateTempProject();
		try
		{
			var output = Path.Combine( root, "Assets", "Generated", "tailbox.scss" );

			Assert.IsTrue( TailBoxProjectFileSystem.WriteIfChanged( output, "first" ) );
			Assert.IsFalse( TailBoxProjectFileSystem.WriteIfChanged( output, "first" ) );
			Assert.IsTrue( TailBoxProjectFileSystem.WriteIfChanged( output, "second" ) );
			Assert.AreEqual( "second", File.ReadAllText( output ) );
		}
		finally
		{
			DeleteTempProject( root );
		}
	}

	[TestMethod]
	public void ResolveOutputPathUsesDefaultsForNullOrBlankConfig()
	{
		var root = CreateTempProject();
		try
		{
			var expected = Path.GetFullPath( Path.Combine( root, "Code", "tailwand.generated.scss" ) );
			Assert.AreEqual( expected, TailBoxProjectFileSystem.ResolveOutputPath( root, null ) );

			var config = TailBoxConfig.CreateDefault();
			config.OutputPath = " ";
			Assert.AreEqual( expected, TailBoxProjectFileSystem.ResolveOutputPath( root, config ) );
		}
		finally
		{
			DeleteTempProject( root );
		}
	}

	[TestMethod]
	public void ShouldSkipPathRejectsOutputHiddenBuildAndOutsidePaths()
	{
		var root = CreateTempProject();
		try
		{
			var output = Path.Combine( root, "Code", "tailwand.generated.scss" );
			var ordinary = Path.Combine( root, "Code", "Screen.razor" );
			var outside = Path.Combine( Path.GetDirectoryName( root )!, Guid.NewGuid().ToString( "N" ), "Screen.razor" );

			Assert.IsTrue( TailBoxProjectFileSystem.ShouldSkipPath( root, output, output ) );
			Assert.IsTrue( TailBoxProjectFileSystem.ShouldSkipPath( root, output, Path.Combine( root, ".git", "Screen.razor" ) ) );
			Assert.IsTrue( TailBoxProjectFileSystem.ShouldSkipPath( root, output, Path.Combine( root, ".sbox", "Screen.razor" ) ) );
			Assert.IsTrue( TailBoxProjectFileSystem.ShouldSkipPath( root, output, Path.Combine( root, "bin", "Screen.razor" ) ) );
			Assert.IsTrue( TailBoxProjectFileSystem.ShouldSkipPath( root, output, outside ) );
			Assert.IsTrue( TailBoxProjectFileSystem.ShouldSkipPath( "", output, ordinary ) );
			Assert.IsFalse( TailBoxProjectFileSystem.ShouldSkipPath( root, output, ordinary ) );
		}
		finally
		{
			DeleteTempProject( root );
		}
	}

	private static void WriteFile( string root, string relativePath, string text )
	{
		var path = Path.Combine( root, relativePath.Replace( '/', Path.DirectorySeparatorChar ) );
		Directory.CreateDirectory( Path.GetDirectoryName( path )! );
		File.WriteAllText( path, text );
	}

	private static string CreateTempProject()
	{
		return TailBoxTestPaths.CreateTempProject();
	}

	private static void DeleteTempProject( string root )
	{
		if ( Directory.Exists( root ) )
		{
			Directory.Delete( root, true );
		}
	}
}