// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.IO;
using Cake.Core.Configuration.Parser;
using Cake.Core.Tests.Properties;
using Cake.Testing;
using Xunit;

namespace Cake.Core.Tests.Unit.Configuration.Parser
{
    public sealed class ConfigurationParserTests
    {
        public sealed class TheParseMethod
        {
            [Fact]
            public void Should_Throw_If_File_Do_Not_Exist()
            {
                // Given
                var environment = FakeEnvironment.CreateUnixEnvironment();
                var fileSystem = new FakeFileSystem(environment);
                var parser = new ConfigurationParser(fileSystem, environment);

                // When
                var result = Record.Exception(() => parser.Read("./cake.config"));

                // Then
                Assert.IsType<FileNotFoundException>(result);
                Assert.Equal("Unable to find the configuration file.", result?.Message);
                Assert.Equal("/Working/cake.config", ((FileNotFoundException)result)?.FileName);
            }

            [Fact]
            public void Should_Throw_If_Section_Contains_Whitespace()
            {
                // Given
                var environment = FakeEnvironment.CreateUnixEnvironment();
                var fileSystem = new FakeFileSystem(environment);
                fileSystem.CreateFile("/Working/cake.config").SetContent("[The Section]");
                var parser = new ConfigurationParser(fileSystem, environment);

                // When
                var result = Record.Exception(() => parser.Read("./cake.config"));

                // Then
                AssertEx.IsExceptionWithMessage<InvalidOperationException>(result, "Sections cannot contain whitespace.");
            }

            [Fact]
            public void Should_Throw_If_Equals_Sign_Is_Missing_From_Key_And_Value_Pair()
            {
                // Given
                var environment = FakeEnvironment.CreateUnixEnvironment();
                var fileSystem = new FakeFileSystem(environment);
                fileSystem.CreateFile("/Working/cake.config").SetContent("Hello");
                var parser = new ConfigurationParser(fileSystem, environment);

                // When
                var result = Record.Exception(() => parser.Read("./cake.config"));

                // Then
                AssertEx.IsExceptionWithMessage<InvalidOperationException>(result, "Expected to find '=' token.");
            }

            [Fact]
            public void Should_Throw_If_Key_Contains_WhiteSpace()
            {
                // Given
                var environment = FakeEnvironment.CreateUnixEnvironment();
                var fileSystem = new FakeFileSystem(environment);
                fileSystem.CreateFile("/Working/cake.config").SetContent("Hello World=True");
                var parser = new ConfigurationParser(fileSystem, environment);

                // When
                var result = Record.Exception(() => parser.Read("./cake.config"));

                // Then
                AssertEx.IsExceptionWithMessage<InvalidOperationException>(result, "The key 'Hello World' contains whitespace.");
            }

            [Fact]
            public void Should_Throw_If_KeyValue_Pair_Is_Not_Followed_By_Section_Or_Another_KeyValue_Pair()
            {
                // Given
                var environment = FakeEnvironment.CreateUnixEnvironment();
                var fileSystem = new FakeFileSystem(environment);
                fileSystem.CreateFile("/Working/cake.config").SetContent("Hello=World\n=True");
                var parser = new ConfigurationParser(fileSystem, environment);

                // When
                var result = Record.Exception(() => parser.Read("./cake.config"));

                // Then
                AssertEx.IsExceptionWithMessage<InvalidOperationException>(result, "Encountered unexpected token.");
            }

            [Fact]
            public void Should_Parse_Ini_With_Sections_Correctly()
            {
                // Given
                var environment = FakeEnvironment.CreateUnixEnvironment();
                var fileSystem = new FakeFileSystem(environment);
                fileSystem.CreateFile("/Working/cake.config").SetContent(Resources.Ini_Configuration);
                var parser = new ConfigurationParser(fileSystem, environment);

                // When
                var result = parser.Read("/Working/cake.config");

                // Then
                Assert.True(result.ContainsKey("Section1_Foo"));
                Assert.Equal("Bar", result["Section1_Foo"]);
                Assert.True(result.ContainsKey("Section2_Baz"));
                Assert.Equal("Qux", result["Section2_Baz"]);
            }

            public sealed class EnvironmentVariableSubstitution
            {
                [Theory]
                [MemberData(nameof(EnvironmentVariableSubstitutionTestData))]
                public void Should_Substitute_Environment_Variables(EnvironmentVariableTestHarness harness)
                {
                    // Given
                    var environment = harness.CreateEnvironment();
                    var fileSystem = new FakeFileSystem(environment);
                    fileSystem.CreateFile("/Working/cake.config").SetContent(harness.IniFileContents);
                    var parser = new ConfigurationParser(fileSystem, environment);

                    // When
                    var result = parser.Read("/Working/cake.config");

                    // Then
                    harness.Assert(result);
                }

                public static IEnumerable<object[]> EnvironmentVariableSubstitutionTestData()
                {
                    yield return new object[]
                    {
                            new EnvironmentVariableTestHarness()
                            {
                                Testcase = $"Basic environment variable substitution",
                                CreateEnvironment = () => FakeEnvironment.CreateUnixEnvironment().AddEnvironmentVariable("VALUE", "world"),
                                IniFileContents = "[Section1]\nHello=%VALUE%",
                                Assert = result => Assert.Equal("world", result["Section1_Hello"])
                            },
                    };

                    yield return new object[]
                    {
                            new EnvironmentVariableTestHarness()
                            {
                                Testcase = $"Casing of substitution token should not matter",
                                CreateEnvironment = () => FakeEnvironment.CreateUnixEnvironment().AddEnvironmentVariable("VALUE", "world"),
                                IniFileContents = "[Section1]\nHello=%value%",
                                Assert = result => Assert.Equal("world", result["Section1_Hello"])
                            },
                    };

                    yield return new object[]
                    {
                            new EnvironmentVariableTestHarness()
                            {
                                Testcase = $"Should respect leading text",
                                CreateEnvironment = () => FakeEnvironment.CreateUnixEnvironment().AddEnvironmentVariable("VALUE", "world"),
                                IniFileContents = "[Section1]\nHello=it is a wonderful %VALUE%",
                                Assert = result => Assert.Equal("it is a wonderful world", result["Section1_Hello"])
                            },
                    };

                    yield return new object[]
                    {
                            new EnvironmentVariableTestHarness()
                            {
                                Testcase = $"Should respect trailing text",
                                CreateEnvironment = () => FakeEnvironment.CreateUnixEnvironment().AddEnvironmentVariable("VALUE", "John"),
                                IniFileContents = "[Section1]\nHello=%VALUE%, nice to meet you.",
                                Assert = result => Assert.Equal("John, nice to meet you.", result["Section1_Hello"])
                            },
                    };

                    yield return new object[]
                    {
                            new EnvironmentVariableTestHarness()
                            {
                                Testcase = $"No environment variable found for substitution token, should not substitute",
                                CreateEnvironment = () => FakeEnvironment.CreateUnixEnvironment(),
                                IniFileContents = "[Section1]\nHello=%VALUE%",
                                Assert = result => Assert.Equal("%VALUE%", result["Section1_Hello"])
                            },
                    };

                    yield return new object[]
                    {
                            new EnvironmentVariableTestHarness()
                            {
                                Testcase = $"Special characters should be allowed",
                                CreateEnvironment = () => FakeEnvironment.CreateUnixEnvironment().AddEnvironmentVariable("ProgramFiles(x86)", "PATH TO PROGRAM FILES"),
                                IniFileContents = "[Section1]\nSpecialPath=%ProgramFiles(x86)%",
                                Assert = result => Assert.Equal("PATH TO PROGRAM FILES", result["Section1_SpecialPath"])
                            },
                    };

                    yield return new object[]
                    {
                            new EnvironmentVariableTestHarness()
                            {
                                Testcase = $"More than one environment variable to substitute, should substitute all",
                                CreateEnvironment = () => FakeEnvironment.CreateUnixEnvironment()
                                                            .AddEnvironmentVariable("VARIABLE1", "Value1")
                                                            .AddEnvironmentVariable("VARIABLE2", "Value2"),
                                IniFileContents = "[Section1]\nValue1=%VARIABLE1%\nValue2=%VARIABLE2%",
                                Assert = result =>
                                {
                                    Assert.Equal("Value1", result["Section1_Value1"]);
                                    Assert.Equal("Value2", result["Section1_Value2"]);
                                }
                            },
                    };
                }

                public class EnvironmentVariableTestHarness
                {
                    public string Testcase { get; set; }

                    public Func<FakeEnvironment> CreateEnvironment { get; set; }

                    public string IniFileContents { get; set; }

                    public Action<IDictionary<string, string>> Assert { get; set; }

                    public override string ToString()
                    {
                        return $"Testcase={Testcase}";
                    }
                }
            }
        }
    }

    internal static class FakeEnvironmentExtensions
    {
        internal static FakeEnvironment AddEnvironmentVariable(this FakeEnvironment environment, string variable, string value)
        {
            environment.SetEnvironmentVariable(variable, value);
            return environment;
        }
    }
}