using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using Xunit;

using Amazon.Lambda.Tools.TemplateProcessor;
using ThirdParty.Json.LitJson;
using YamlDotNet.RepresentationModel;
using System.Linq;
using static Amazon.Lambda.Tools.TemplateProcessor.JsonTemplateParser;
using static Amazon.Lambda.Tools.TemplateProcessor.YamlTemplateParser;

namespace Amazon.Lambda.Tools.Test
{
    public class TemplateParserTests
    {
        [Fact]
        public void DetermineJsonReader()
        {
            Assert.IsType<JsonTemplateParser>(TemplateProcessorManager.CreateTemplateParser("{\"AWSTemplateFormatVersion\" : \"2010-09-09\"}"));
            Assert.IsType<JsonTemplateParser>(TemplateProcessorManager.CreateTemplateParser("\t\t{\"AWSTemplateFormatVersion\" : \"2010-09-09\"}"));
            Assert.IsType<JsonTemplateParser>(TemplateProcessorManager.CreateTemplateParser("\n{\"AWSTemplateFormatVersion\" : \"2010-09-09\"}"));
        }

        [Fact]
        public void DetermineYamlReader()
        {
            Assert.IsType<YamlTemplateParser>(TemplateProcessorManager.CreateTemplateParser("AWSTemplateFormatVersion: \"2010-09-09\""));
            Assert.IsType<YamlTemplateParser>(TemplateProcessorManager.CreateTemplateParser("-"));
        }

        [Theory]
        [MemberData(nameof(GetAndSetValuesOnRootData))]
        public void GetAndSetValuesOnRoot(IUpdatableResourceDataSource source)
        {
            Assert.Equal("/home/code", source.GetValue("CodeUri"));

            source.SetValue("s3://my-bucket/my-key", "CodeUri");
            Assert.Equal("s3://my-bucket/my-key", source.GetValue("CodeUri"));
        }

        public static IEnumerable<object[]> GetAndSetValuesOnRootData()
        {
            var list = new List<object[]>();
            {
                var rootData = new JsonData();
                rootData["CodeUri"] = "/home/code";
                var source = new JsonTemplateParser.JsonUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }
            {
                var rootData = new YamlMappingNode();
                rootData.Children.Add("CodeUri", new YamlScalarNode("/home/code"));

                var source = new YamlTemplateParser.YamlUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }

            return list;
        }


        [Theory]
        [MemberData(nameof(GetAndSetValuesOnChildOnObjectData))]
        public void GetAndSetValuesOnChildOnObject(IUpdatableResourceDataSource source)
        {
            Assert.Equal("/currentProject", source.GetValue("Code", "S3Key"));

            source.SetValue("my-key", "Code", "S3Key");
            Assert.Equal("my-key", source.GetValue("Code", "S3Key"));
        }

        public static IEnumerable<object[]> GetAndSetValuesOnChildOnObjectData()
        {
            var list = new List<object[]>();
            {
                var codeData = new JsonData();
                codeData["S3Bucket"] = "";
                codeData["S3Key"] = "/currentProject";

                var rootData = new JsonData();
                rootData["Code"] = codeData;

                var source = new JsonTemplateParser.JsonUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }
            {
                var codeData = new YamlMappingNode();
                codeData.Children.Add("S3Bucket", new YamlScalarNode(""));
                codeData.Children.Add("S3Key", new YamlScalarNode("/currentProject"));

                var rootData = new YamlMappingNode();
                rootData.Children.Add("Code", codeData);

                var source = new YamlTemplateParser.YamlUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }

            return list;
        }

        [Theory]
        [MemberData(nameof(GetNullValueAndSetValuesOnRootOnObjectData))]
        public void GetNullValueAndSetValuesOnRootOnObject(IUpdatableResourceDataSource source)
        {
            Assert.Null(source.GetValue("CodeUri"));

            source.SetValue("my-key", "CodeUri");
            Assert.Equal("my-key", source.GetValue("CodeUri"));
        }

        public static IEnumerable<object[]> GetNullValueAndSetValuesOnRootOnObjectData()
        {
            var list = new List<object[]>();
            {
                var rootData = new JsonData();

                var source = new JsonTemplateParser.JsonUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }
            {
                var rootData = new YamlMappingNode();

                var source = new YamlTemplateParser.YamlUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }

            return list;
        }

        [Theory]
        [MemberData(nameof(GetNullValueAndSetValuesOnChildOnObjectData))]
        public void GetNullValueAndSetValuesOnChildOnObject(IUpdatableResourceDataSource source)
        {
            Assert.Null(source.GetValue("Code", "S3Key"));

            source.SetValue("my-key", "Code", "S3Key");
            Assert.Equal("my-key", source.GetValue("Code", "S3Key"));
        }

        public static IEnumerable<object[]> GetNullValueAndSetValuesOnChildOnObjectData()
        {
            var list = new List<object[]>();
            {
                var rootData = new JsonData();

                var source = new JsonTemplateParser.JsonUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }
            {
                var rootData = new YamlMappingNode();

                var source = new YamlTemplateParser.YamlUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }

            return list;
        }


        [Fact]
        public void ServerlessFunction_GetCurrentDirectoryForWithNullCodeUri()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                                    new Dictionary<string, string>
                                    {
                                    });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_SERVERLESS_FUNCTION, dataSource);



            Assert.Equal(".", resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "my.zip");
            Assert.Equal("s3://my-bucket/my.zip", dataSource.GetValue("CodeUri"));
        }

        [Theory]
        [InlineData(".")]
        [InlineData("/home")]
        public void ServerlessFunction_GetLocalPath(string localPath)
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                                    new Dictionary<string, string>
                                    {
                                        {"CodeUri", localPath }
                                    });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_SERVERLESS_FUNCTION, dataSource);

            Assert.Equal(localPath, resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "my.zip");
            Assert.Equal("s3://my-bucket/my.zip", dataSource.GetValue("CodeUri"));
        }

        [Fact]
        public void ServerlessFunction_NotUpdatableBecausePointAtAlreadyS3()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                                    new Dictionary<string, string>
                                    {
                                        {"CodeUri", "s3://my-bucket/my.zip" }
                                    });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_SERVERLESS_FUNCTION, dataSource);

            Assert.Null(resource.Fields[0].GetLocalPath());
        }

        [Fact]
        public void LambdaFunction_GetCurrentDirectoryForWithNullCode()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                                    new Dictionary<string, string>
                                    {
                                    });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_LAMBDA_FUNCTION, dataSource);

            Assert.Equal(".", resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "my.zip");
            Assert.Equal("my-bucket", dataSource.GetValue("Code/S3Bucket"));
            Assert.Equal("my.zip", dataSource.GetValue("Code/S3Key"));
        }

        [Theory]
        [InlineData(".")]
        [InlineData("/home")]
        public void LambdaFunction_GetLocalPath(string localPath)
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                                    new Dictionary<string, string>
                                    {
                                        {"Code/S3Key", localPath }
                                    });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_LAMBDA_FUNCTION, dataSource);

            Assert.Equal(localPath, resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "my.zip");
            Assert.Equal("my-bucket", dataSource.GetValue("Code/S3Bucket"));
            Assert.Equal("my.zip", dataSource.GetValue("Code/S3Key"));
        }

        [Theory]
        [InlineData(".")]
        [InlineData("/home")]
        public void LambdaFunction_GetLocalPathAndEmptyS3Bucket(string localPath)
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                                    new Dictionary<string, string>
                                    {
                                        {"Code/S3Bucket", "" },
                                        {"Code/S3Key", localPath }
                                    });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_LAMBDA_FUNCTION, dataSource);

            Assert.Equal(localPath, resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "my.zip");
            Assert.Equal("my-bucket", dataSource.GetValue("Code/S3Bucket"));
            Assert.Equal("my.zip", dataSource.GetValue("Code/S3Key"));
        }

        [Fact]
        public void LambdaFunction_NotUpdatableBecausePointAtAlreadyS3()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                                    new Dictionary<string, string>
                                    {
                                        {"Code/S3Bucket", "my-bucket" },
                                        {"Code/S3Key", "my.zip" }
                                    });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_LAMBDA_FUNCTION, dataSource);

            Assert.Null(resource.Fields[0].GetLocalPath());
        }
        
        [Fact]
        public void ApiGatewayApi_GetCurrentDirectoryForWithNullCode()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_SERVERLESS_API, dataSource);

            Assert.Null(resource.Fields[0].GetLocalPath());
        }
        
        [Theory]
        [InlineData("/home/swagger.yml")]
        public void ApiGatewayApi_GetLocalPathAndEmptyS3Bucket(string localPath)
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                    {"BodyS3Location/Key", localPath }
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_APIGATEWAY_RESTAPI, dataSource);

            Assert.Equal(localPath, resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", localPath);
            Assert.Equal("my-bucket", dataSource.GetValue("BodyS3Location/Bucket"));
            Assert.Equal(localPath, dataSource.GetValue("BodyS3Location/Key"));
        }
        
        [Fact]
        public void AppSyncGraphQl_GetCurrentDirectoryForWithNullCode()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_APPSYNC_GRAPHQLSCHEMA, dataSource);

            Assert.Null(resource.Fields[0].GetLocalPath());
        }
        
        [Theory]
        [InlineData("/home/swagger.yml")]
        public void AppSyncGraphQl_GetLocalPathAndEmptyS3Bucket(string localPath)
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                    {"DefinitionS3Location", localPath }
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_APPSYNC_GRAPHQLSCHEMA, dataSource);

            Assert.Equal(localPath, resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "swagger.yml");
            Assert.Equal("s3://my-bucket/swagger.yml", dataSource.GetValue("DefinitionS3Location"));
        }
        

        [Fact]
        public void AppSyncResolver_GetCurrentDirectoryForWithNullCode()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_APPSYNC_RESOLVER, dataSource);

            Assert.Null(resource.Fields[0].GetLocalPath());
            Assert.Null(resource.Fields[1].GetLocalPath());
        }
        
        [Fact]
        public void AppSyncResolver_GetLocalPathAndEmptyS3Bucket()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                    {"ResponseMappingTemplateS3Location", "response.xml" },
                    {"RequestMappingTemplateS3Location", "request.xml" }
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_APPSYNC_RESOLVER, dataSource);

            Assert.Equal("response.xml", resource.Fields[0].GetLocalPath());
            Assert.Equal("request.xml", resource.Fields[1].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "response.xml-updated");
            Assert.Equal("s3://my-bucket/response.xml-updated", dataSource.GetValue("ResponseMappingTemplateS3Location"));
            resource.Fields[1].SetS3Location("my-bucket", "request.xml-updated");
            Assert.Equal("s3://my-bucket/request.xml-updated", dataSource.GetValue("RequestMappingTemplateS3Location"));
        }
        
        [Fact]
        public void ServerlessApi_GetCurrentDirectoryForWithNullCode()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_APIGATEWAY_RESTAPI, dataSource);

            Assert.Null(resource.Fields[0].GetLocalPath());
        }
        
        [Theory]
        [InlineData("/home/swagger.yml")]
        public void ServerlessApi_GetLocalPathAndEmptyS3Bucket(string localPath)
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                    {"DefinitionUri", localPath }
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_SERVERLESS_API, dataSource);

            Assert.Equal(localPath, resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "swagger.yml");
            Assert.Equal("s3://my-bucket/swagger.yml", dataSource.GetValue("DefinitionUri"));
        }        
        
        
        [Fact]
        public void ElasticBeanstalkApplicationVersion_GetCurrentDirectoryForWithNullCode()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_ELASTICBEANSTALK_APPLICATIONVERSION, dataSource);

            Assert.Null(resource.Fields[0].GetLocalPath());
        }
        
        [Theory]
        [InlineData("/home/app.zip")]
        public void ElasticBeanstalkApplicationVersion_GetLocalPathAndEmptyS3Bucket(string localPath)
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                    {"SourceBundle/S3Key", localPath }
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_ELASTICBEANSTALK_APPLICATIONVERSION, dataSource);

            Assert.Equal(localPath, resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", localPath);
            Assert.Equal("my-bucket", dataSource.GetValue("SourceBundle/S3Bucket"));
            Assert.Equal(localPath, dataSource.GetValue("SourceBundle/S3Key"));
        }  
        
        
        [Fact]
        public void CloudFormationStack_GetCurrentDirectoryForWithNullCode()
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_CLOUDFORMATION_STACK, dataSource);

            Assert.Null(resource.Fields[0].GetLocalPath());
        }

        [Theory]
        [MemberData(nameof(IgnoreResourceWithInlineCodeData))]
        public void IgnoreResourceWithInlineCode(IUpdatableResourceDataSource source)
        {
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_LAMBDA_FUNCTION, source);
            Assert.False(resource.Fields[0].IsCode);
        }


        public static IEnumerable<object[]> IgnoreResourceWithInlineCodeData()
        {
            const string InlineCode = @"{ 'Fn::Join': ['', [
  'var response = require('cfn-response');',
  'exports.handler = function(event, context) {',
  '  var input = parseInt(event.ResourceProperties.Input);',
  '  var responseData = {Value: input * 5};',
  '  response.send(event, context, response.SUCCESS, responseData);',
  '};'
]]}";
            var list = new List<object[]>();
            {
                var codeData = new JsonData();
                codeData["ZipFile"] = InlineCode;

                var rootData = new JsonData();
                rootData["Code"] = codeData;

                var source = new JsonTemplateParser.JsonUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }
            {
                var codeData = new YamlMappingNode();
                codeData.Children.Add("ZipFile", new YamlScalarNode(InlineCode));

                var rootData = new YamlMappingNode();
                rootData.Children.Add("Code", codeData);

                var source = new YamlTemplateParser.YamlUpdatableResourceDataSource(null, null, rootData);
                list.Add(new object[] { source });
            }

            return list;
        }

        [Theory]
        [InlineData("/home/infra.template")]
        public void CloudFormationStack_GetLocalPathAndEmptyS3Bucket(string localPath)
        {
            var dataSource = new FakeUpdatableResourceDataSource(
                new Dictionary<string, string>
                {
                    {"TemplateUrl", localPath }
                });
            var resource = new UpdatableResource("TestResource", UpdatableResourceDefinition.DEF_CLOUDFORMATION_STACK, dataSource);

            Assert.Equal(localPath, resource.Fields[0].GetLocalPath());

            resource.Fields[0].SetS3Location("my-bucket", "swagger.yml");
            Assert.Equal("s3://my-bucket/swagger.yml", dataSource.GetValue("TemplateUrl"));
        }

        [Fact]
        public void TestGetDictionaryFromResourceForJsonTemplate()
        {
            var assembly = this.GetType().GetTypeInfo().Assembly;
            var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/ImageBasedProjects/ServerlessTemplateExamples");
            var cloudFormationTemplate = "serverless-resource-dockerbuildargs-json.template";
            string templateBody = File.ReadAllText(Path.Combine(fullPath, cloudFormationTemplate));
            var root = JsonMapper.ToObject(templateBody);
            var firstResource = root["Resources"][0];
            var jsonDataSource = new JsonUpdatableResourceDataSource(null, firstResource, null);
            var valueDictionaryFromResource = jsonDataSource.GetValueDictionaryFromResource("Metadata", "DockerBuildArgs");

            Assert.NotNull(valueDictionaryFromResource);
            Assert.Equal(2, valueDictionaryFromResource.Count);
            Assert.Equal("/src/path-to/project", valueDictionaryFromResource["PROJECT_PATH"]);
            Assert.Equal("project.csproj", valueDictionaryFromResource["PROJECT_FILE"]);
        }

        [Fact]
        public void TestGetDictionaryFromResourceForYamlTemplate()
        {
            var assembly = this.GetType().GetTypeInfo().Assembly;
            var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/ImageBasedProjects/ServerlessTemplateExamples");
            var cloudFormationTemplate = "serverless-resource-dockerbuildargs-yaml.template";
            string templateBody = File.ReadAllText(Path.Combine(fullPath, cloudFormationTemplate));
            YamlStream yamlStream = new YamlStream();
            yamlStream.Load(new StringReader(templateBody));

            var root = (YamlMappingNode)yamlStream.Documents[0].RootNode;
            var resourcesKey = new YamlScalarNode("Resources");
            var resources = (YamlMappingNode)root.Children[resourcesKey];

            var firstResource = (YamlMappingNode)resources.Children.First().Value;
            var yamlDataSource = new YamlUpdatableResourceDataSource(null, firstResource, null);
            var valueDictionaryFromResource = yamlDataSource.GetValueDictionaryFromResource("Metadata", "DockerBuildArgs");

            Assert.NotNull(valueDictionaryFromResource);
            Assert.Equal(2, valueDictionaryFromResource.Count);
            Assert.Equal("/src/path-to/project", valueDictionaryFromResource["PROJECT_PATH"]);
            Assert.Equal("project.csproj", valueDictionaryFromResource["PROJECT_FILE"]);
        }

        public class FakeUpdatableResourceDataSource : IUpdatableResourceDataSource
        {
            IDictionary<string, string> Root { get; }
            IDictionary<string, string> Properties { get; }

            public FakeUpdatableResourceDataSource(IDictionary<string, string> root, IDictionary<string, string> properties)
            {
                this.Root = root;
                this.Properties = properties;
            }

            public FakeUpdatableResourceDataSource(IDictionary<string, string> properties)
                : this(properties, properties)
            {
            }

            public string GetValueFromRoot(params string[] keyPath)
            {
                return GetValue(this.Root, keyPath);
            }
            
            public string[] GetValueListFromRoot(params string[] keyPath)
            {
                return GetValueList(this.Root, keyPath);
            }

            public string GetValue(params string[] keyPath)
            {
                return GetValue(this.Properties, keyPath);
            }

            private string GetValue(IDictionary<string, string> values, params string[] keyPath)
            {
                var key = string.Join("/", keyPath);
                if (values.TryGetValue(key, out var value))
                    return value;
                return null;
            }
            
            public string[] GetValueList(params string[] keyPath)
            {
                return GetValueList(this.Properties, keyPath);
            }

            private string[] GetValueList(IDictionary<string, string> values, params string[] keyPath)
            {
                var key = string.Join("/", keyPath);
                if (values.TryGetValue(key, out var value))
                    return value.Split(',');
                return null;
            }            

            public void SetValue(string value, params string[] keyPath)
            {
                var key = string.Join("/", keyPath);
                Properties[key] = value;
            }
            
            public void SetValueList(string[] values, params string[] keyPath)
            {
                var key = string.Join("/", keyPath);
                Properties[key] = string.Join(',', values);
            }

            public string GetValueFromResource(params string[] keyPath)
            {
                throw new System.NotImplementedException();
            }

            public Dictionary<string, string> GetValueDictionaryFromResource(params string[] keyPath)
            {
                throw new System.NotImplementedException();
            }
        }
    }
}