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

using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Razor.Infrastructure;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
    public class DefaultFileVersionProviderTest
    {
        [Theory]
        [InlineData("/hello/world", "/hello/world?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk")]
        [InlineData("/hello/world?q=test", "/hello/world?q=test&v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk")]
        [InlineData("/hello/world?q=foo&bar", "/hello/world?q=foo&bar&v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk")]
        [InlineData("/hello/world?q=foo&bar#abc", "/hello/world?q=foo&bar&v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk#abc")]
        [InlineData("/hello/world#somefragment", "/hello/world?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk#somefragment")]
        public void AddFileVersionToPath_WhenCacheIsAbsent(string filePath, string expected)
        {
            // Arrange
            var fileProvider = GetMockFileProvider(filePath);
            var fileVersionProvider = GetFileVersionProvider(fileProvider);
            var requestPath = GetRequestPathBase();

            // Act
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, filePath);

            // Assert
            Assert.Equal(expected, result);
        }

        [Fact]
        public void AddFileVersionToPath_CachesNotFoundResults()
        {
            // Arrange
            var path = "/wwwroot/file.txt";
            var fileProvider = GetMockFileProvider(
                path,
                pathStartsWithAppName: false,
                fileDoesNotExist: true);
            var fileVersionProvider = GetFileVersionProvider(fileProvider);
            var mockFileProvider = Mock.Get(fileProvider);
            var requestPath = GetRequestPathBase();

            // Act 1
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, path);

            // Assert 1
            Assert.Equal(path, result);
            mockFileProvider.Verify(f => f.GetFileInfo(It.IsAny<string>()), Times.Once());
            mockFileProvider.Verify(f => f.Watch(It.IsAny<string>()), Times.Once());

            // Act 2
            result = fileVersionProvider.AddFileVersionToPath(requestPath, path);

            // Assert 2
            Assert.Equal(path, result);
            mockFileProvider.Verify(f => f.GetFileInfo(It.IsAny<string>()), Times.Once());
            mockFileProvider.Verify(f => f.Watch(It.IsAny<string>()), Times.Once());
        }

        [Theory]
        [InlineData("file.txt", false)]
        [InlineData("/wwwroot/file.txt", true)]
        public void AddFileVersionToPath_CachesFoundResults(string path, bool pathStartsWithAppName)
        {
            // Arrange
            var fileProvider = GetMockFileProvider(
                "file.txt",
                pathStartsWithAppName);
            var fileVersionProvider = GetFileVersionProvider(fileProvider);
            var mockFileProvider = Mock.Get(fileProvider);
            var requestPath = GetRequestPathBase();

            // Act 1
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, path);

            // Assert 1
            Assert.Equal($"{path}?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", result);
            mockFileProvider.Verify(f => f.GetFileInfo(It.IsAny<string>()), Times.Once());
            mockFileProvider.Verify(f => f.Watch(It.IsAny<string>()), Times.Once());

            // Act 2
            result = fileVersionProvider.AddFileVersionToPath(requestPath, path);

            // Assert 2
            Assert.Equal($"{path}?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", result);
            mockFileProvider.Verify(f => f.GetFileInfo(It.IsAny<string>()), Times.Once());
            mockFileProvider.Verify(f => f.Watch(It.IsAny<string>()), Times.Once());
        }

        [Fact]
        public void AddFileVersionToPath_UpdatesEntryWhenCacheExpires_ForNonExistingFile()
        {
            // Arrange
            var fileProvider = new TestFileProvider();
            var fileVersionProvider = GetFileVersionProvider(fileProvider);
            var requestPath = GetRequestPathBase();

            // Act 1 - File does not exist
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, "file.txt");

            // Assert 1
            Assert.Equal("file.txt", result);

            // Act 2 - File gets added
            fileProvider.AddFile("file.txt", "Hello World!");
            fileProvider.GetChangeToken("file.txt").HasChanged = true;
            result = fileVersionProvider.AddFileVersionToPath(requestPath, "file.txt");

            // Assert 2
            Assert.Equal("file.txt?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", result);
        }

        [Fact]
        public void AddFileVersionToPath_UpdatesEntryWhenCacheExpires_ForExistingFile()
        {
            // Arrange
            var fileProvider = new TestFileProvider();
            var requestPath = GetRequestPathBase();
            var fileVersionProvider = GetFileVersionProvider(fileProvider);
            fileProvider.AddFile("file.txt", "Hello World!");

            // Act 1 - File exists
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, "file.txt");

            // Assert 1
            Assert.Equal("file.txt?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", result);

            // Act 2
            fileProvider.DeleteFile("file.txt");
            fileProvider.GetChangeToken("file.txt").HasChanged = true;
            result = fileVersionProvider.AddFileVersionToPath(requestPath, "file.txt");

            // Assert 2
            Assert.Equal("file.txt", result);
        }

        [Fact]
        public void AddFileVersionToPath_UpdatesEntryWhenCacheExpires_ForExistingFile_WithRequestPathBase()
        {
            // Arrange
            var fileProvider = new TestFileProvider();
            var requestPath = GetRequestPathBase("/wwwroot/");
            var fileVersionProvider = GetFileVersionProvider(fileProvider);
            fileProvider.AddFile("file.txt", "Hello World!");

            // Act 1 - File exists
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, "/wwwroot/file.txt");

            // Assert 1
            Assert.Equal("/wwwroot/file.txt?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", result);

            // Act 2
            fileProvider.DeleteFile("file.txt");
            fileProvider.GetChangeToken("file.txt").HasChanged = true;
            result = fileVersionProvider.AddFileVersionToPath(requestPath, "/wwwroot/file.txt");

            // Assert 2
            Assert.Equal("/wwwroot/file.txt", result);
        }

        // Verifies if the stream is closed after reading.
        [Fact]
        public void AddFileVersionToPath_DoesNotLockFileAfterReading()
        {
            // Arrange
            var stream = new TestableMemoryStream(Encoding.UTF8.GetBytes("Hello World!"));
            var mockFile = new Mock<IFileInfo>();
            mockFile.SetupGet(f => f.Exists).Returns(true);
            mockFile
                .Setup(m => m.CreateReadStream())
                .Returns(stream);

            var fileProvider = new TestFileProvider();
            fileProvider.AddFile("/hello/world", mockFile.Object);
            var requestPath = GetRequestPathBase();
            var fileVersionProvider = GetFileVersionProvider(fileProvider);

            // Act
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, "/hello/world");

            // Assert
            Assert.True(stream.Disposed);
        }

        [Theory]
        [InlineData("/testApp/hello/world", true, "/testApp")]
        [InlineData("/testApp/foo/bar/hello/world", true, "/testApp/foo/bar")]
        [InlineData("/test/testApp/hello/world", false, "/testApp")]
        public void AddFileVersionToPath_PathContainingAppName(
            string filePath,
            bool pathStartsWithAppBase,
            string requestPathBase)
        {
            // Arrange
            var fileProvider = GetMockFileProvider(filePath, pathStartsWithAppBase);
            var requestPath = GetRequestPathBase(requestPathBase);
            var fileVersionProvider = GetFileVersionProvider(fileProvider);

            // Act
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, filePath);

            // Assert
            Assert.Equal(filePath + "?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", result);
        }

        [Fact]
        public void DoesNotAddVersion_IfFileNotFound()
        {
            // Arrange
            var filePath = "http://contoso.com/hello/world";
            var fileProvider = GetMockFileProvider(filePath, false, true);
            var requestPath = GetRequestPathBase();
            var fileVersionProvider = GetFileVersionProvider(fileProvider);

            // Act
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, filePath);

            // Assert
            Assert.Equal("http://contoso.com/hello/world", result);
        }

        [Fact]
        public void ReturnsValueFromCache()
        {
            // Arrange
            var filePath = "/hello/world";
            var fileProvider = GetMockFileProvider(filePath);
            var fileVersionProvider = GetFileVersionProvider(fileProvider);
            var cacheEntryOptions = new MemoryCacheEntryOptions();
            cacheEntryOptions.SetSize(1);
            fileVersionProvider.Cache.Set(filePath, "FromCache", cacheEntryOptions);
            var requestPath = GetRequestPathBase();

            // Act
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, filePath);

            // Assert
            Assert.Equal("FromCache", result);
        }

        [Fact]
        public void AddFileVersionToPath_CachesEntry() => AddFileVersionToPath("/hello/world", "/hello/world", null);

        [Fact]
        public void AddFileVersionToPath_WithRequestPathBase_CachesEntry() => AddFileVersionToPath("/testApp/hello/world", "/hello/world", "/testApp");

        private static void AddFileVersionToPath(string filePath, string watchPath, string requestPathBase)
        {
            // Arrange
            var expected = filePath + "?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk";
            var expectedSize = expected.Length * sizeof(char);
            var changeToken = Mock.Of<IChangeToken>();

            var fileProvider = GetMockFileProvider(filePath, requestPathBase != null);
            Mock.Get(fileProvider)
                .Setup(f => f.Watch(watchPath)).Returns(changeToken);

            var cacheEntry = Mock.Of<ICacheEntry>(c => c.ExpirationTokens == new List<IChangeToken>());
            var cache = new Mock<IMemoryCache>();

            cache.Setup(c => c.CreateEntry(filePath))
                .Returns(cacheEntry)
                .Verifiable();

            var requestPath = GetRequestPathBase(requestPathBase);

            var fileVersionProvider = GetFileVersionProvider(fileProvider, cache.Object);

            // Act
            var result = fileVersionProvider.AddFileVersionToPath(requestPath, filePath);

            // Assert
            Assert.Equal(expected, result);
            Assert.Equal(expected, cacheEntry.Value);
            Assert.Equal(expectedSize, cacheEntry.Size);
            cache.VerifyAll();
        }

        private static DefaultFileVersionProvider GetFileVersionProvider(
            IFileProvider fileProvider,
            IMemoryCache memoryCache = null)
        {
            var hostingEnv = Mock.Of<IWebHostEnvironment>(e => e.WebRootFileProvider == fileProvider);
            var cacheProvider = new TagHelperMemoryCacheProvider();
            if (memoryCache != null)
            {
                cacheProvider.Cache = memoryCache;
            }

            return new DefaultFileVersionProvider(hostingEnv, cacheProvider);
        }

        private static IFileProvider GetMockFileProvider(
            string filePath,
            bool pathStartsWithAppName = false,
            bool fileDoesNotExist = false)
        {
            var existingMockFile = new Mock<IFileInfo>();
            existingMockFile.SetupGet(f => f.Exists).Returns(true);
            existingMockFile
                .Setup(m => m.CreateReadStream())
                .Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")));

            var doesNotExistMockFile = new Mock<IFileInfo>();
            doesNotExistMockFile.SetupGet(f => f.Exists).Returns(false);

            var mockFileProvider = new Mock<IFileProvider>();
            if (pathStartsWithAppName)
            {
                mockFileProvider.Setup(fp => fp.GetFileInfo(filePath)).Returns(doesNotExistMockFile.Object);
                mockFileProvider.Setup(fp => fp.GetFileInfo(It.Is<string>(str => str != filePath)))
                    .Returns(existingMockFile.Object);
            }
            else
            {
                mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny<string>()))
                    .Returns(fileDoesNotExist ? doesNotExistMockFile.Object : existingMockFile.Object);
            }

            mockFileProvider.Setup(fp => fp.Watch(It.IsAny<string>()))
                .Returns(new TestFileChangeToken());

            return mockFileProvider.Object;
        }

        private static PathString GetRequestPathBase(string requestPathBase = null)
        {
            return new PathString(requestPathBase);
        }

        private class TestableMemoryStream : MemoryStream
        {
            public TestableMemoryStream(byte[] buffer)
                : base(buffer)
            {
            }

            public bool Disposed { get; private set; }

            protected override void Dispose(bool disposing)
            {
                base.Dispose(disposing);
                Disposed = true;
            }
        }
    }
}
