<commit_before>﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NUnit.Framework;
using Rocks.Builders;
using Rocks.Configuration;
using System.CodeDom.Compiler;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Reflection;

namespace Rocks.Tests;

// TODO: These methods just create code that generator driver can work off of,
// but we actually don't compile the resultant assembly with that new generated code,
// and we need to such that we make sure it is actually able to compile.
public static class CodeGenerationTests
{
	[TestCase(typeof(object))]
	[TestCase(typeof(Dictionary<,>))]
	[TestCase(typeof(ImmutableArray))]
	[TestCase(typeof(HttpMessageHandler))]
	public static void GenerateCreatesForBaseClassLibrary(Type targetType) =>
		CodeGenerationTests.GenerateForBaseClassLibrary(targetType, new RockCreateGenerator());

	[TestCase(typeof(object))]
	[TestCase(typeof(Dictionary<,>))]
	[TestCase(typeof(ImmutableArray))]
	[TestCase(typeof(HttpMessageHandler))]
	public static void GenerateMakesForBaseClassLibrary(Type targetType) =>
		CodeGenerationTests.GenerateForBaseClassLibrary(targetType, new RockMakeGenerator());

	private static void GenerateForBaseClassLibrary(Type targetType, IIncrementalGenerator generator)
	{
		if (targetType is null)
		{
			throw new ArgumentNullException(nameof(targetType));
		}

		var types = new ConcurrentBag<Type>();
		Parallel.ForEach(targetType.Assembly.GetTypes()
			.Where(_ => _.IsPublic && !_.IsSealed), _ =>
			{
				if (_.IsValidTarget())
				{
					types.Add(_);
				}
			});

		var discoveredTypes = types.ToArray();

		var code = CodeGenerationTests.GetCode(types, discoveredTypes);
		var syntaxTree = CSharpSyntaxTree.ParseText(code);
		var references = AppDomain.CurrentDomain.GetAssemblies()
			.Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location))
			.Select(_ => MetadataReference.CreateFromFile(_.Location))
			.Concat(new[]
			{
				MetadataReference.CreateFromFile(targetType.Assembly.Location),
				MetadataReference.CreateFromFile(typeof(Rock).Assembly.Location),
			});
		var compilation = CSharpCompilation.Create("generator", new[] { syntaxTree },
			references, new(OutputKind.DynamicallyLinkedLibrary));

		var driver = CSharpGeneratorDriver.Create(generator);
		driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics);

		Assert.Multiple(() =>
		{
			Assert.That(discoveredTypes.Length, Is.GreaterThan(0));
			Assert.That(diagnostics.Any(
				_ => _.Severity == DiagnosticSeverity.Error || _.Severity == DiagnosticSeverity.Warning), Is.False);
		});
	}

	private static string GetCode(ConcurrentBag<Type> types, Type[] discoveredTypes)
	{
		using var writer = new StringWriter();
		using var indentWriter = new IndentedTextWriter(writer, "\t");
		indentWriter.WriteLine("using Rocks;");
		indentWriter.WriteLine();
		indentWriter.WriteLine("public static class GenerateCode");
		indentWriter.WriteLine("{");
		indentWriter.Indent++;

		indentWriter.WriteLine("public static void Go()");
		indentWriter.WriteLine("{");
		indentWriter.Indent++;

		for (var i = 0; i < types.Count; i++)
		{
			indentWriter.WriteLine($"var r{i} = Rock.Create<{discoveredTypes[i].GetTypeDefinition()}>();");
		}

		indentWriter.Indent--;
		indentWriter.WriteLine("}");

		indentWriter.Indent--;
		indentWriter.WriteLine("}");

		return writer.ToString();
	}

	private static bool IsValidTarget(this Type self)
	{
		var code = $"public class Foo {{ public {self.GetTypeDefinition()} Data {{ get; }} }}";
		var syntaxTree = CSharpSyntaxTree.ParseText(code);
		var references = AppDomain.CurrentDomain.GetAssemblies()
			.Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location))
			.Select(_ => MetadataReference.CreateFromFile(_.Location))
			.Concat(new[] { MetadataReference.CreateFromFile(self.Assembly.Location) });
		var compilation = CSharpCompilation.Create("generator", new[] { syntaxTree },
			references, new(OutputKind.DynamicallyLinkedLibrary));
		var model = compilation.GetSemanticModel(syntaxTree, true);

		var propertySyntax = syntaxTree.GetRoot().DescendantNodes(_ => true)
			.OfType<PropertyDeclarationSyntax>().Single();
		var symbol = model.GetDeclaredSymbol(propertySyntax)!.Type;
		var information = new MockInformation(symbol!, compilation.Assembly, model,
			new ConfigurationValues(IndentStyle.Tab, 3, true), BuildType.Create);

		return !information.Diagnostics.Any(_ => _.Severity == DiagnosticSeverity.Error);
	}

	/// <summary>
	/// This isn't complete. There may be constraints that won't be satisfied with this.
	/// I'll tackle them as they come.
	/// </summary>
	private static string GetTypeDefinition(this Type self)
	{
		if (self.IsGenericTypeDefinition)
		{
			var selfGenericArguments = self.GetGenericArguments();
			var genericArguments = new string[selfGenericArguments.Length];

			for (var i = 0; i < selfGenericArguments.Length; i++)
			{
				var argument = selfGenericArguments[0];
				var argumentAttributes = argument.GenericParameterAttributes & GenericParameterAttributes.SpecialConstraintMask;

				if (argumentAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint))
				{
					genericArguments[i] = "int";
				}
				else if (argumentAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint))
				{
					genericArguments[i] = "object";
				}
				else
				{
					genericArguments[i] = "object";
				}
			}

			return $"{self.FullName!.Split("`")[0]}<{string.Join(", ", genericArguments)}>";
		}
		else
		{
			return self.FullName!;
		}
	}
}<commit_msg>#167 fixed the gen'd code to do a create or a make<commit_after>﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NUnit.Framework;
using Rocks.Builders;
using Rocks.Configuration;
using System.CodeDom.Compiler;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Reflection;

namespace Rocks.Tests;

// TODO: These methods just create code that generator driver can work off of,
// but we actually don't compile the resultant assembly with that new generated code,
// and we need to such that we make sure it is actually able to compile.
public static class CodeGenerationTests
{
	[TestCase(typeof(object))]
	[TestCase(typeof(Dictionary<,>))]
	[TestCase(typeof(ImmutableArray))]
	[TestCase(typeof(HttpMessageHandler))]
	public static void GenerateCreatesForBaseClassLibrary(Type targetType) =>
		CodeGenerationTests.GenerateForBaseClassLibrary(targetType, new RockCreateGenerator());

	[TestCase(typeof(object))]
	[TestCase(typeof(Dictionary<,>))]
	[TestCase(typeof(ImmutableArray))]
	[TestCase(typeof(HttpMessageHandler))]
	public static void GenerateMakesForBaseClassLibrary(Type targetType) =>
		CodeGenerationTests.GenerateForBaseClassLibrary(targetType, new RockMakeGenerator());

	private static void GenerateForBaseClassLibrary(Type targetType, IIncrementalGenerator generator)
	{
		if (targetType is null)
		{
			throw new ArgumentNullException(nameof(targetType));
		}

		var isCreate = generator is RockCreateGenerator;

		var types = new ConcurrentBag<Type>();
		Parallel.ForEach(targetType.Assembly.GetTypes()
			.Where(_ => _.IsPublic && !_.IsSealed), _ =>
			{
				if (_.IsValidTarget())
				{
					types.Add(_);
				}
			});

		var discoveredTypes = types.ToArray();

		var code = CodeGenerationTests.GetCode(types, discoveredTypes, isCreate);
		var syntaxTree = CSharpSyntaxTree.ParseText(code);
		var references = AppDomain.CurrentDomain.GetAssemblies()
			.Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location))
			.Select(_ => MetadataReference.CreateFromFile(_.Location))
			.Concat(new[]
			{
				MetadataReference.CreateFromFile(targetType.Assembly.Location),
				MetadataReference.CreateFromFile(typeof(Rock).Assembly.Location),
			});
		var compilation = CSharpCompilation.Create("generator", new[] { syntaxTree },
			references, new(OutputKind.DynamicallyLinkedLibrary));

		var driver = CSharpGeneratorDriver.Create(generator);
		driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics);

		Assert.Multiple(() =>
		{
			Assert.That(discoveredTypes.Length, Is.GreaterThan(0));
			Assert.That(diagnostics.Any(
				_ => _.Severity == DiagnosticSeverity.Error || _.Severity == DiagnosticSeverity.Warning), Is.False);
		});
	}

	private static string GetCode(ConcurrentBag<Type> types, Type[] discoveredTypes, bool isCreate)
	{
		using var writer = new StringWriter();
		using var indentWriter = new IndentedTextWriter(writer, "\t");
		indentWriter.WriteLine("using Rocks;");
		indentWriter.WriteLine();
		indentWriter.WriteLine("public static class GenerateCode");
		indentWriter.WriteLine("{");
		indentWriter.Indent++;

		indentWriter.WriteLine("public static void Go()");
		indentWriter.WriteLine("{");
		indentWriter.Indent++;

		for (var i = 0; i < types.Count; i++)
		{
			indentWriter.WriteLine($"var r{i} = Rock.{(isCreate ? "Create" : "Make")}<{discoveredTypes[i].GetTypeDefinition()}>();");
		}

		indentWriter.Indent--;
		indentWriter.WriteLine("}");

		indentWriter.Indent--;
		indentWriter.WriteLine("}");

		return writer.ToString();
	}

	private static bool IsValidTarget(this Type self)
	{
		var code = $"public class Foo {{ public {self.GetTypeDefinition()} Data {{ get; }} }}";
		var syntaxTree = CSharpSyntaxTree.ParseText(code);
		var references = AppDomain.CurrentDomain.GetAssemblies()
			.Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location))
			.Select(_ => MetadataReference.CreateFromFile(_.Location))
			.Concat(new[] { MetadataReference.CreateFromFile(self.Assembly.Location) });
		var compilation = CSharpCompilation.Create("generator", new[] { syntaxTree },
			references, new(OutputKind.DynamicallyLinkedLibrary));
		var model = compilation.GetSemanticModel(syntaxTree, true);

		var propertySyntax = syntaxTree.GetRoot().DescendantNodes(_ => true)
			.OfType<PropertyDeclarationSyntax>().Single();
		var symbol = model.GetDeclaredSymbol(propertySyntax)!.Type;
		var information = new MockInformation(symbol!, compilation.Assembly, model,
			new ConfigurationValues(IndentStyle.Tab, 3, true), BuildType.Create);

		return !information.Diagnostics.Any(_ => _.Severity == DiagnosticSeverity.Error);
	}

	/// <summary>
	/// This isn't complete. There may be constraints that won't be satisfied with this.
	/// I'll tackle them as they come.
	/// </summary>
	private static string GetTypeDefinition(this Type self)
	{
		if (self.IsGenericTypeDefinition)
		{
			var selfGenericArguments = self.GetGenericArguments();
			var genericArguments = new string[selfGenericArguments.Length];

			for (var i = 0; i < selfGenericArguments.Length; i++)
			{
				var argument = selfGenericArguments[0];
				var argumentAttributes = argument.GenericParameterAttributes & GenericParameterAttributes.SpecialConstraintMask;

				if (argumentAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint))
				{
					genericArguments[i] = "int";
				}
				else if (argumentAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint))
				{
					genericArguments[i] = "object";
				}
				else
				{
					genericArguments[i] = "object";
				}
			}

			return $"{self.FullName!.Split("`")[0]}<{string.Join(", ", genericArguments)}>";
		}
		else
		{
			return self.FullName!;
		}
	}
}
{'fraction_non_alphanumeric': 0.12545739675901724, 'fraction_numerical': 0.0013068478829064297, 'mean_word_length': 5.21830985915493, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1198824', 'n_tokens_mistral': 3671, 'n_tokens_neox': 3245, 'n_words': 705}