using NodePipeline.Abstractions.Interfaces.Nodes;
using NodePipeline.Engine.CodeGeneration;
using NodePipeline.Engine.CodeGeneration.Abstractions.Models;
using NodePipeline.Engine.Tests.CodeGeneratorTests.Fixtures;
using NodePipeline.Engine.Tests.CodeGeneratorTests.Fixtures.MockNodes;

namespace NodePipeline.Engine.Tests.CodeGeneratorTests;

public class NodeValidatorGeneratorTests
{
    #region GetAllPorts

    [Fact]
    public void GetAllPortsMethod_ShouldExistInGeneratedCode()
    {
        var methodSignatureV1 =
            "private IReadOnlyDictionary<NodePortKey, NodePortInfo> GetAllPorts(string pipelineId, INodeFactory nodeFactory, List<NodeConfig> allNodes)";
        var methodSignatureV2 =
            "private IReadOnlyDictionary<NodePortKey, NodePortInfo> GetAllPorts(string pipelineId, INodeFactory nodeFactory,"
            + Environment.NewLine + "        List<NodeConfig> allNodes)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.True(generatedCode.Contains(methodSignatureV1)
                    || generatedCode.Contains(methodSignatureV2));
    }

    #endregion

    #region GetNodeFieldValidator

    [Fact]
    public void GetNodeFieldValidatorMethod_ShouldExistInGeneratedCode()
    {
        var methodSignature = "private INodeFieldValidator<T> GetNodeFieldValidator<T>(Type validatorType)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.Contains(methodSignature, generatedCode);
    }

    #endregion

    #region ValidateNodePorts

    [Fact]
    public void ValidateNodePortsMethod_ShouldExistInGeneratedCode()
    {
        var methodSignatureV1 =
            "private ValidationResult ValidateNodePorts(string pipelineId, INodeFactory nodeFactory, string nodeType, string nodeId, IReadOnlyDictionary<string, InputSource> nodePortConnections, IReadOnlyDictionary<NodePortKey, NodePortInfo> ports, out Dictionary<string, NodeFieldValidationResult> inputPortValidationResults, CultureInfo cultureInfo)";
        var methodSignatureV2 =
            "private ValidationResult ValidateNodePorts(string pipelineId, INodeFactory nodeFactory, string nodeType, string nodeId,"
            + Environment.NewLine +
            "        IReadOnlyDictionary<string, InputSource> nodePortConnections, IReadOnlyDictionary<NodePortKey, NodePortInfo> ports,"
            + Environment.NewLine +
            "        out Dictionary<string, List<NodeFieldValidationResult>> inputPortValidationResults, CultureInfo cultureInfo)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.True(generatedCode.Contains(methodSignatureV1)
                    || generatedCode.Contains(methodSignatureV2));
    }

    #endregion

    #region ValidateInputPort

    [Fact]
    public void ValidateInputPortMethod_ShouldExistInGeneratedCode()
    {
        var methodSignatureV1 =
            "private NodeFieldValidationResult ValidateInputPort(string nodeId, NodePortInfo inputPort, InputSource connection, IReadOnlyDictionary<NodePortKey, NodePortInfo> ports, CultureInfo cultureInfo)";
        var methodSignatureV2 =
            "private NodeFieldValidationResult ValidateInputPort(string nodeId, NodePortInfo inputPort, InputSource connection,"
            + Environment.NewLine +
            "        IReadOnlyDictionary<NodePortKey, NodePortInfo> ports, CultureInfo cultureInfo)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.True(generatedCode.Contains(methodSignatureV1)
                    || generatedCode.Contains(methodSignatureV2));
    }

    #endregion

    #region GetNodeValidator

    [Fact]
    public void GetNodeValidatorMethod_ShouldExistInGeneratedCode()
    {
        var methodSignature = "private INodeValidator GetNodeValidator(Type validatorType)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.Contains(methodSignature, generatedCode);
    }

    #endregion

    #region CheckRequiredNodeParameter

    [Fact]
    public void CheckRequiredNodeParameterMethod_ShouldExistInGeneratedCode()
    {
        var methodSignature =
            "private NodeFieldValidationResult CheckRequiredNodeParameter<T>(string nodeId, NodePipeline.Abstractions.Models.NodeField<T> nodeField, string nodeParameterName, CultureInfo cultureInfo)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.Contains(methodSignature, generatedCode);
    }

    #endregion

    #region SuccessNodeValidationResult

    [Fact]
    public void SuccessNodeValidationResultMethod_ShouldExistInGeneratedCode()
    {
        var methodSignature =
            "private static NodeValidationResult SuccessNodeValidationResult(bool hasWarnings, Dictionary<string, List<NodeFieldValidationResult>> fieldResults, Dictionary<string, List<NodeFieldValidationResult>> portResults)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.Contains(methodSignature, generatedCode);
    }

    #endregion

    #region GetErrorValidationResult

    [Fact]
    public void GetErrorValidationResultMethod_ShouldExistInGeneratedCode()
    {
        var methodSignature =
            "private NodeValidationResult GetErrorValidationResult(ValidationResult validationResult, string nodeId, Dictionary<string, List<NodeFieldValidationResult>> fieldResults, Dictionary<string, List<NodeFieldValidationResult>> portResults, CultureInfo cultureInfo)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.Contains(methodSignature, generatedCode);
    }

    #endregion

    #region SuccessNodeFieldValidationResult

    [Fact]
    public void SuccessNodeFieldValidationResultMethod_ShouldExistInGeneratedCode()
    {
        var methodSignature = "private static NodeFieldValidationResult SuccessNodeFieldValidationResult()";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.Contains(methodSignature, generatedCode);
    }

    #endregion

    #region UpdateValidationResult

    [Fact]
    public void UpdateValidationResultMethod_ShouldExistInGeneratedCode()
    {
        var methodSignatureV1 =
            "private static void UpdateValidationResult(ValidationResult validationResult, ref ValidationResult parametersValidationResult)";
        var methodSignatureV2 = "private static void UpdateValidationResult(ValidationResult validationResult,"
                                + Environment.NewLine + "        ref ValidationResult parametersValidationResult)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.True(generatedCode.Contains(methodSignatureV1)
                    || generatedCode.Contains(methodSignatureV2));
    }

    #endregion

    #region ValidateNode

    [Fact]
    public void ValidateNodeMethod_ShouldExistInGeneratedCode()
    {
        var methodSignatureV1 =
            "public NodeValidationResult ValidateNode(string pipelineId, INodeFactory nodeFactory, NodeConfig nodeConfig, List<NodeConfig> allNodes, Dictionary<string, object?> parameters, CultureInfo cultureInfo)";
        var methodSignatureV2 =
            "public NodeValidationResult ValidateNode(string pipelineId, INodeFactory nodeFactory, NodeConfig nodeConfig," +
            Environment.NewLine +
            "List<NodeConfig> allNodes, Dictionary<string, object?> parameters, CultureInfo cultureInfo)";
        // var methodSignature = $"private static Dictionary<NodePortKey, NodePortInfo> Get__{GetNodeDescriptorFromMockNode(mockNodeType).TypeNameShortSanitized}__NodePorts(string nodeId)";
        var model = TestNodeModelFactory.CreateMultipleNodesModel();

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.True(generatedCode.Contains(methodSignatureV1)
                    || generatedCode.Contains(methodSignatureV2));
    }

    [Fact]
    public void InitializeNodeFieldsMethod_ContainsSwitchCases()
    {
        var model = TestNodeModelFactory.CreateMultipleNodesModel();
        var methodName = "ValidateNode";
        var tCountByNodes = new Dictionary<NodeDescriptor, int>();

        var generatedCode = NodeValidatorGenerator.Generate(model);
        var methodCode = TestHelper.ExtractMethod(generatedCode, methodName);

        var firstTextV1 = "var node = nodeFactory.CreateNode(pipelineId, nodeConfig);"
                          + Environment.NewLine +
                          "            nodeFactory.SetNodeParametersValues(pipelineId, node, nodeConfig.Id, parameters);"
                          + Environment.NewLine + "        var nodeConnections = allNodes.SelectMany(q => q.Inputs"
                          + Environment.NewLine +
                          "                .Select(x => new KeyValuePair<string, InputSource>(GetNodeInputKey(q.Id, x.Key), x.Value))"
                          + Environment.NewLine + "            ).ToDictionary(q => q.Key, q => q.Value);"
                          + Environment.NewLine + "        var ports = GetAllPorts(pipelineId, nodeFactory, allNodes);";
        var firstTextV2 = "var node = nodeFactory.CreateNode(pipelineId, nodeConfig);"
                          + Environment.NewLine +
                          "        nodeFactory.SetNodeParametersValues(pipelineId, node, nodeConfig.Id, parameters);"
                          + Environment.NewLine +
                          "        var nodeConnections = allNodes.SelectMany(q => q.Inputs.Select(x => new KeyValuePair<string, InputSource>(GetNodeInputKey(q.Id, x.Key), x.Value))).ToDictionary(q => q.Key, q => q.Value);"
                          + Environment.NewLine + "        var ports = GetAllPorts(pipelineId, nodeFactory, allNodes);";
        Assert.True(methodCode.Contains(firstTextV1)
                    || methodCode.Contains(firstTextV2));
        foreach (var node in model.Nodes)
        {
            var @case = $"case {node.TypeNameFull} t";
            var caseLabel = methodCode[methodCode.IndexOf(@case, StringComparison.Ordinal)..].Split(':')[0];
            var tNumberString = caseLabel[@case.Length..];
            var caseBlock = TestHelper.ExtractSwitchCase(methodCode, @case);
            Assert.False(string.IsNullOrEmpty(caseBlock), $"Case block for node {node.Type} not found.");

            // var expectedCode = $"return Validate__{node.TypeNameShortSanitized}__Node(pipelineId, nodeFactory, t{tNumberString}, nodeConfig.Id, nodeConnections, ports, cultureInfo);";
            // Assert.Contains(expectedCode, caseBlock);
            Assert.Contains(
                $"return Validate__{node.TypeNameShortSanitized}__Node(pipelineId, nodeFactory, t{tNumberString}, nodeConfig.Id, nodeConnections, ports, cultureInfo);",
                caseBlock);
            if (!tCountByNodes.TryGetValue(node, out var value)) tCountByNodes.Add(node, 1);
            else tCountByNodes[node] = ++value;
        }

        Assert.Contains(
            "default:" + Environment.NewLine +
            "                throw new UnknownNodeTypeException(nodeConfig.Type, nodeConfig.Id);", methodCode);
        Assert.DoesNotContain(tCountByNodes.Values, q => q > 1);
    }

    #endregion

    #region ValidateNodeSpecific

    [Theory]
    [InlineData(typeof(ThreeParameterNode), "CreateThreeParameterNodeModel")]
    [InlineData(typeof(ThreePortNode), "CreateThreePortNodeModel")]
    [InlineData(typeof(SimpleNamedNode), "CreateSimpleNamedNodeModel")]
    public void SpecificValidateNodeMethod_ShouldExistInGeneratedCode(Type mockNodeType, string factoryMethod)
    {
        var nodeDescriptor = TestHelper.GetNodeDescriptorFromMockNode(mockNodeType);
        var methodSignatureV1 =
            $"private NodeValidationResult Validate__{nodeDescriptor.TypeNameShortSanitized}__Node(string pipelineId, INodeFactory nodeFactory, {nodeDescriptor.TypeNameFull} node,"
            + Environment.NewLine +
            "        string nodeId, IReadOnlyDictionary<string, InputSource> nodePortConnections, IReadOnlyDictionary<NodePortKey, NodePortInfo> ports, CultureInfo cultureInfo)";
        var methodSignatureV2 =
            $"private NodeValidationResult Validate__{nodeDescriptor.TypeNameShortSanitized}__Node(string pipelineId, INodeFactory nodeFactory, {nodeDescriptor.TypeNameFull} node, string nodeId, IReadOnlyDictionary<string, InputSource> nodePortConnections, IReadOnlyDictionary<NodePortKey, NodePortInfo> ports, CultureInfo cultureInfo)";
        var model = TestHelper.GetNodeModel(factoryMethod);

        var generatedCode = NodeValidatorGenerator.Generate(model);

        Assert.True(generatedCode.Contains(methodSignatureV1)
                    || generatedCode.Contains(methodSignatureV2));
    }

    [Fact]
    public void SpecificValidateNodeMethod_ShouldCallCheckRequiredNodeParameterOnlyForRequiredFields()
    {
        var nodeDescriptor = ThreeParameterNode.GetDescriptor();
        var methodName = $"Validate__{nodeDescriptor.TypeNameShortSanitized}__Node";

        var model = TestNodeModelFactory.CreateThreeParameterNodeModel();
        var generatedCode = NodeValidatorGenerator.Generate(model);
        var methodCode = TestHelper.ExtractMethod(generatedCode, methodName);

        foreach (var parameter in nodeDescriptor.Fields.Where(q => q.Direction == FieldDirection.Parameter))
        {
            var expectedSecondPart =
                $"CheckRequiredNodeParameter<{parameter.ValueType}>(nodeId, node.{parameter.PropertyName}, \"{parameter.FieldName}\", cultureInfo);";
            if (parameter.Metadata?.IsRequired == true)
                Assert.Contains(expectedSecondPart, methodCode);
            else
                Assert.DoesNotContain(expectedSecondPart, methodCode);
        }
    }

    [Fact]
    public void SpecificValidateNodeMethod_ShouldContainAppropriateValidatorAtPropertyWithStringLengthConstraint()
    {
        string MethodName(NodeDescriptor n)
        {
            return $"Validate__{n.TypeNameShortSanitized}__Node";
        }

        string ExpectedString(NodeFieldDescriptor f)
        {
            return
                $"= new StringLengthValidator(PipelineLocalizationProvider, {TestHelper.NumberToString(f.Metadata!.StringMinLength)}, {TestHelper.NumberToString(f.Metadata!.StringMaxLength)}).Validate(node.{f.PropertyName});";
        }

        var nodeDescriptor1 = ThreeParameterNode.GetDescriptor();
        var nodeDescriptor2 = ThreeParameterNode2.GetDescriptor();
        var nodeDescriptor3 = ThreeParameterNode3.GetDescriptor();

        var model = TestNodeModelFactory.CreateMultipleNodesWithStringParameterLengthConstraints();
        var generatedCode = NodeValidatorGenerator.Generate(model);

        var m1 = TestHelper.ExtractMethod(generatedCode, MethodName(nodeDescriptor2));
        var f1 = nodeDescriptor2.Fields.First(q => q.PropertyName == nameof(ThreeParameterNode2.Parameter3));
        var m2 = TestHelper.ExtractMethod(generatedCode, MethodName(nodeDescriptor1));
        var f2 = nodeDescriptor1.Fields.First(q => q.PropertyName == nameof(ThreeParameterNode.Parameter2));
        var m3 = TestHelper.ExtractMethod(generatedCode, MethodName(nodeDescriptor3));
        var f3 = nodeDescriptor3.Fields.First(q => q.PropertyName == nameof(ThreeParameterNode3.Parameter3));
        var es1 = ExpectedString(f1);
        var es2 = ExpectedString(f2);
        var es3 = ExpectedString(f3);

        //check minlength constraint
        Assert.Contains(es1, m1);
        //check maxlength constraint
        Assert.Contains(es2, m2);
        //check length between constraint
        Assert.Contains(es3, m3);
    }

    [Fact]
    public void SpecificValidateNodeMethod_ShouldContainAppropriateValidatorAtPropertyWithIntValidationConstraint()
    {
        string MethodName(NodeDescriptor n)
        {
            return $"Validate__{n.TypeNameShortSanitized}__Node";
        }

        string ExpectedString(NodeFieldDescriptor f)
        {
            return
                $"= new NumberRangeValidator<{f.ValueType}>(PipelineLocalizationProvider, {TestHelper.NumberToString(f.Metadata!.NumberMinBound)}, {TestHelper.NumberToString(f.Metadata!.NumberMaxBound)}).Validate(node.{f.PropertyName});";
        }

        var nodeDescriptor1 = ThreeParameterNode.GetDescriptor();
        var nodeDescriptor2 = ThreeParameterNode2.GetDescriptor();

        var model = TestNodeModelFactory.CreateMultipleNodesWithStringParameterLengthConstraints();
        var generatedCode = NodeValidatorGenerator.Generate(model);

        var m1 = TestHelper.ExtractMethod(generatedCode, MethodName(nodeDescriptor2));
        var f1 = nodeDescriptor2.Fields.First(q => q.PropertyName == nameof(ThreeParameterNode2.Parameter1));
        var m2 = TestHelper.ExtractMethod(generatedCode, MethodName(nodeDescriptor2));
        var f2 = nodeDescriptor2.Fields.First(q => q.PropertyName == nameof(ThreeParameterNode2.Parameter2));
        var m3 = TestHelper.ExtractMethod(generatedCode, MethodName(nodeDescriptor1));
        var f3 = nodeDescriptor1.Fields.First(q => q.PropertyName == nameof(ThreeParameterNode.Parameter1));
        var es1 = ExpectedString(f1);
        var es2 = ExpectedString(f2);
        var es3 = ExpectedString(f3);

        //check minlength constraint
        Assert.Contains(es1, m1);
        //check maxlength constraint
        Assert.Contains(es2, m2);
        //check length between constraint
        Assert.Contains(es3, m3);
    }

    #endregion
}