// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Xunit;

namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
{
    public class KeyPerFileTests
    {
        [Fact]
        public void DoesNotThrowWhenOptionalAndNoSecrets()
        {
            new ConfigurationBuilder().AddKeyPerFile(o => o.Optional = true).Build();
        }

        [Fact]
        public void DoesNotThrowWhenOptionalAndDirectoryDoesntExist()
        {
            new ConfigurationBuilder().AddKeyPerFile("nonexistent", true).Build();
        }

        [Fact]
        public void ThrowsWhenNotOptionalAndDirectoryDoesntExist()
        {
            var e = Assert.Throws<ArgumentException>(() => new ConfigurationBuilder().AddKeyPerFile("nonexistent", false).Build());
            Assert.Contains("The path must be absolute.", e.Message);
        }

        [Fact]
        public void CanLoadMultipleSecrets()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("Secret1", "SecretValue1"),
                new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o => o.FileProvider = testFileProvider)
                .Build();

            Assert.Equal("SecretValue1", config["Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);
        }

        [Fact]
        public void CanLoadMultipleSecretsWithDirectory()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("Secret1", "SecretValue1"),
                new TestFile("Secret2", "SecretValue2"),
                new TestFile("directory"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o => o.FileProvider = testFileProvider)
                .Build();

            Assert.Equal("SecretValue1", config["Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);
        }

        [Fact]
        public void CanLoadNestedKeys()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("Secret0__Secret1__Secret2__Key", "SecretValue2"),
                new TestFile("Secret0__Secret1__Key", "SecretValue1"),
                new TestFile("Secret0__Key", "SecretValue0"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o => o.FileProvider = testFileProvider)
                .Build();

            Assert.Equal("SecretValue0", config["Secret0:Key"]);
            Assert.Equal("SecretValue1", config["Secret0:Secret1:Key"]);
            Assert.Equal("SecretValue2", config["Secret0:Secret1:Secret2:Key"]);
        }

        [Fact]
        public void LoadWithCustomSectionDelimiter()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("Secret0--Secret1--Secret2--Key", "SecretValue2"),
                new TestFile("Secret0--Secret1--Key", "SecretValue1"),
                new TestFile("Secret0--Key", "SecretValue0"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.SectionDelimiter = "--";
                })
                .Build();

            Assert.Equal("SecretValue0", config["Secret0:Key"]);
            Assert.Equal("SecretValue1", config["Secret0:Secret1:Key"]);
            Assert.Equal("SecretValue2", config["Secret0:Secret1:Secret2:Key"]);
        }

        [Fact]
        public void CanIgnoreFilesWithDefault()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("ignore.Secret0", "SecretValue0"),
                new TestFile("ignore.Secret1", "SecretValue1"),
                new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o => o.FileProvider = testFileProvider)
                .Build();

            Assert.Null(config["ignore.Secret0"]);
            Assert.Null(config["ignore.Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);
        }

        [Fact]
        public void CanTurnOffDefaultIgnorePrefixWithCondition()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("ignore.Secret0", "SecretValue0"),
                new TestFile("ignore.Secret1", "SecretValue1"),
                new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.IgnoreCondition = null;
                })
                .Build();

            Assert.Equal("SecretValue0", config["ignore.Secret0"]);
            Assert.Equal("SecretValue1", config["ignore.Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);
        }

        [Fact]
        public void CanIgnoreAllWithCondition()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("Secret0", "SecretValue0"),
                new TestFile("Secret1", "SecretValue1"),
                new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.IgnoreCondition = s => true;
                })
                .Build();

            Assert.Empty(config.AsEnumerable());
        }

        [Fact]
        public void CanIgnoreFilesWithCustomIgnore()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("meSecret0", "SecretValue0"),
                new TestFile("meSecret1", "SecretValue1"),
                new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.IgnorePrefix = "me";
                })
                .Build();

            Assert.Null(config["meSecret0"]);
            Assert.Null(config["meSecret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);
        }

        [Fact]
        public void CanUnIgnoreDefaultFiles()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("ignore.Secret0", "SecretValue0"),
                new TestFile("ignore.Secret1", "SecretValue1"),
                new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.IgnorePrefix = null;
                })
                .Build();

            Assert.Equal("SecretValue0", config["ignore.Secret0"]);
            Assert.Equal("SecretValue1", config["ignore.Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);
        }

        [Fact]
        public void BindingDoesNotThrowIfReloadedDuringBinding()
        {
            var testFileProvider = new TestFileProvider(
                new TestFile("Number", "-2"),
                new TestFile("Text", "Foo"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o => o.FileProvider = testFileProvider)
                .Build();

            MyOptions options = null;

            using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
            {
                void ReloadLoop()
                {
                    while (!cts.IsCancellationRequested)
                    {
                        config.Reload();
                    }
                }

                _ = Task.Run(ReloadLoop);

                while (!cts.IsCancellationRequested)
                {
                    options = config.Get<MyOptions>();
                }
            }

            Assert.Equal(-2, options.Number);
            Assert.Equal("Foo", options.Text);
        }

        [Fact]
        public void ReloadConfigWhenReloadOnChangeIsTrue()
        {
            var testFileProvider = new TestFileProvider(
               new TestFile("Secret1", "SecretValue1"),
               new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.ReloadOnChange = true;
                }).Build();

            Assert.Equal("SecretValue1", config["Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);

            testFileProvider.ChangeFiles(
                new TestFile("Secret1", "NewSecretValue1"),
                new TestFile("Secret3", "NewSecretValue3"));

            Assert.Equal("NewSecretValue1", config["Secret1"]);
            Assert.Null(config["NewSecret2"]);
            Assert.Equal("NewSecretValue3", config["Secret3"]);
        }

        [Fact]
        public void SameConfigWhenReloadOnChangeIsFalse()
        {
            var testFileProvider = new TestFileProvider(
               new TestFile("Secret1", "SecretValue1"),
               new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.ReloadOnChange = false;
                }).Build();

            Assert.Equal("SecretValue1", config["Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);

            testFileProvider.ChangeFiles(
                new TestFile("Secret1", "NewSecretValue1"),
                new TestFile("Secret3", "NewSecretValue3"));

            Assert.Equal("SecretValue1", config["Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);
        }

        [Fact]
        public void NoFilesReloadWhenAddedFiles()
        {
            var testFileProvider = new TestFileProvider();

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.ReloadOnChange = true;
                    o.Optional = true;
                }).Build();

            Assert.Empty(config.AsEnumerable());

            testFileProvider.ChangeFiles(
                new TestFile("Secret1", "SecretValue1"),
                new TestFile("Secret2", "SecretValue2"));

            Assert.Equal("SecretValue1", config["Secret1"]);
            Assert.Equal("SecretValue2", config["Secret2"]);
        }

        [Fact(Timeout = 2000)]
        public async Task RaiseChangeEventWhenReloadOnChangeIsTrue()
        {
            var testFileProvider = new TestFileProvider(
               new TestFile("Secret1", "SecretValue1"),
               new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.ReloadOnChange = true;
                }).Build();

            var changeToken = config.GetReloadToken();
            var changeTaskCompletion = new TaskCompletionSource<object>();
            changeToken.RegisterChangeCallback(state =>
                ((TaskCompletionSource<object>)state).TrySetResult(null), changeTaskCompletion);

            testFileProvider.ChangeFiles(
                new TestFile("Secret1", "NewSecretValue1"),
                new TestFile("Secret3", "NewSecretValue3"));

            await changeTaskCompletion.Task;

            Assert.Equal("NewSecretValue1", config["Secret1"]);
            Assert.Null(config["NewSecret2"]);
            Assert.Equal("NewSecretValue3", config["Secret3"]);
        }

        [Fact(Timeout = 2000)]
        public async Task RaiseChangeEventWhenDirectoryClearsReloadOnChangeIsTrue()
        {
            var testFileProvider = new TestFileProvider(
               new TestFile("Secret1", "SecretValue1"),
               new TestFile("Secret2", "SecretValue2"));

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.ReloadOnChange = true;
                }).Build();

            var changeToken = config.GetReloadToken();
            var changeTaskCompletion = new TaskCompletionSource<object>();
            changeToken.RegisterChangeCallback(state =>
                ((TaskCompletionSource<object>)state).TrySetResult(null), changeTaskCompletion);

            testFileProvider.ChangeFiles();

            await changeTaskCompletion.Task;

            Assert.Empty(config.AsEnumerable());
        }

        [Fact(Timeout = 2000)]
        public async Task RaiseChangeEventAfterStartingWithEmptyDirectoryReloadOnChangeIsTrue()
        {
            var testFileProvider = new TestFileProvider();

            var config = new ConfigurationBuilder()
                .AddKeyPerFile(o =>
                {
                    o.FileProvider = testFileProvider;
                    o.ReloadOnChange = true;
                    o.Optional = true;
                }).Build();

            var changeToken = config.GetReloadToken();
            var changeTaskCompletion = new TaskCompletionSource<object>();
            changeToken.RegisterChangeCallback(state =>
                ((TaskCompletionSource<object>)state).TrySetResult(null), changeTaskCompletion);

            testFileProvider.ChangeFiles(new TestFile("Secret1", "SecretValue1"));

            await changeTaskCompletion.Task;

            Assert.Equal("SecretValue1", config["Secret1"]);
        }

        [Fact(Timeout = 2000)]
        public async Task RaiseChangeEventAfterProviderSetToNull()
        {
            var testFileProvider = new TestFileProvider(
               new TestFile("Secret1", "SecretValue1"),
               new TestFile("Secret2", "SecretValue2"));
            var configurationSource = new KeyPerFileConfigurationSource
            {
                FileProvider = testFileProvider,
                Optional = true,
            };
            var keyPerFileProvider = new KeyPerFileConfigurationProvider(configurationSource);
            var config = new ConfigurationRoot(new[] { keyPerFileProvider });

            var changeToken = config.GetReloadToken();
            var changeTaskCompletion = new TaskCompletionSource<object>();
            changeToken.RegisterChangeCallback(state =>
                ((TaskCompletionSource<object>)state).TrySetResult(null), changeTaskCompletion);

            configurationSource.FileProvider = null;
            config.Reload();

            await changeTaskCompletion.Task;

            Assert.Empty(config.AsEnumerable());
        }

        private sealed class MyOptions
        {
            public int Number { get; set; }
            public string Text { get; set; }
        }
    }

    class TestFileProvider : IFileProvider
    {
        IDirectoryContents _contents;
        MockChangeToken _changeToken;

        public TestFileProvider(params IFileInfo[] files)
        {
            _contents = new TestDirectoryContents(files);
            _changeToken = new MockChangeToken();
        }

        public IDirectoryContents GetDirectoryContents(string subpath) => _contents;

        public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory");

        public IChangeToken Watch(string filter) => _changeToken;

        internal void ChangeFiles(params IFileInfo[] files)
        {
            _contents = new TestDirectoryContents(files);
            _changeToken.RaiseCallback();
        }
    }

    class MockChangeToken : IChangeToken
    {
        private Action _callback;

        public bool ActiveChangeCallbacks => true;

        public bool HasChanged => true;

        public IDisposable RegisterChangeCallback(Action<object> callback, object state)
        {
            var disposable = new MockDisposable();
            _callback = () => callback(state);
            return disposable;
        }

        internal void RaiseCallback()
        {
            _callback?.Invoke();
        }
    }

    class MockDisposable : IDisposable
    {
        public bool Disposed { get; set; }

        public void Dispose()
        {
            Disposed = true;
        }
    }

    class TestDirectoryContents : IDirectoryContents
    {
        List<IFileInfo> _list;

        public TestDirectoryContents(params IFileInfo[] files)
        {
            _list = new List<IFileInfo>(files);
        }

        public bool Exists => _list.Any();

        public IEnumerator<IFileInfo> GetEnumerator() => _list.GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

    //TODO: Probably need a directory and file type.
    class TestFile : IFileInfo
    {
        private readonly string _name;
        private readonly string _contents;

        public bool Exists => true;

        public bool IsDirectory
        {
            get;
        }

        public DateTimeOffset LastModified => throw new NotImplementedException();

        public long Length => throw new NotImplementedException();

        public string Name => _name;

        public string PhysicalPath => "Root/" + Name;

        public TestFile(string name)
        {
            _name = name;
            IsDirectory = true;
        }

        public TestFile(string name, string contents)
        {
            _name = name;
            _contents = contents;
        }

        public Stream CreateReadStream()
        {
            if (IsDirectory)
            {
                throw new InvalidOperationException("Cannot create stream from directory");
            }

            return _contents == null
                ? new MemoryStream()
                : new MemoryStream(Encoding.UTF8.GetBytes(_contents));
        }
    }
}
