// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
    public class CacheTagKeyTest
    {
        [Fact]
        public void GenerateKey_ReturnsKeyBasedOnTagHelperUniqueId()
        {
            // Arrange
            var id = Guid.NewGuid().ToString();
            var tagHelperContext = GetTagHelperContext(id);
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };
            var expected = "CacheTagHelper||" + id;

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Fact]
        public void Equals_ReturnsTrueOnSameKey()
        {
            // Arrange
            var id = Guid.NewGuid().ToString();
            var tagHelperContext1 = GetTagHelperContext(id);
            var cacheTagHelper1 = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };

            var tagHelperContext2 = GetTagHelperContext(id);
            var cacheTagHelper2 = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };

            // Act
            var cacheTagKey1 = new CacheTagKey(cacheTagHelper1, tagHelperContext1);
            var cacheTagKey2 = new CacheTagKey(cacheTagHelper2, tagHelperContext2);

            // Assert
            Assert.Equal(cacheTagKey1, cacheTagKey2);
        }

        [Fact]
        public void Equals_ReturnsFalseOnDifferentKey()
        {
            // Arrange
            var tagHelperContext1 = GetTagHelperContext("some-id");
            var cacheTagHelper1 = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };

            var tagHelperContext2 = GetTagHelperContext("some-other-id");
            var cacheTagHelper2 = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };

            // Act
            var cacheTagKey1 = new CacheTagKey(cacheTagHelper1, tagHelperContext1);
            var cacheTagKey2 = new CacheTagKey(cacheTagHelper2, tagHelperContext2);

            // Assert
            Assert.NotEqual(cacheTagKey1, cacheTagKey2);
        }

        [Fact]
        public void GetHashCode_IsSameForSimilarCacheTagHelper()
        {
            // Arrange
            var tagHelperContext1 = GetTagHelperContext("some-id");
            var cacheTagHelper1 = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };

            var tagHelperContext2 = GetTagHelperContext("some-id");
            var cacheTagHelper2 = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };

            var cacheKey1 = new CacheTagKey(cacheTagHelper1, tagHelperContext1);
            var cacheKey2 = new CacheTagKey(cacheTagHelper2, tagHelperContext2);

            // Act
            var hashcode1 = cacheKey1.GetHashCode();
            var hashcode2 = cacheKey2.GetHashCode();

            // Assert
            Assert.Equal(hashcode1, hashcode2);
        }

        [Fact]
        public void GetHashCode_VariesByUniqueId()
        {
            // Arrange
            var tagHelperContext1 = GetTagHelperContext("some-id");
            var cacheTagHelper1 = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };

            var tagHelperContext2 = GetTagHelperContext("some-other-id");
            var cacheTagHelper2 = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext()
            };

            var cacheKey1 = new CacheTagKey(cacheTagHelper1, tagHelperContext1);
            var cacheKey2 = new CacheTagKey(cacheTagHelper2, tagHelperContext2);

            // Act
            var hashcode1 = cacheKey1.GetHashCode();
            var hashcode2 = cacheKey2.GetHashCode();

            // Assert
            Assert.NotEqual(hashcode1, hashcode2);
        }

        [Fact]
        public void GenerateKey_ReturnsKeyBasedOnTagHelperName()
        {
            // Arrange
            var name = "some-name";
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new DistributedCacheTagHelper(
                Mock.Of<IDistributedCacheTagHelperService>(),
                new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                Name = name
            };
            var expected = "DistributedCacheTagHelper||" + name;

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper);
            var key = cacheTagKey.GenerateKey();

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

        [Theory]
        [InlineData("Vary-By-Value")]
        [InlineData("Vary  with spaces")]
        [InlineData("  Vary  with more spaces   ")]
        public void GenerateKey_UsesVaryByPropertyToGenerateKey(string varyBy)
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryBy = varyBy
            };
            var expected = "CacheTagHelper||testid||VaryBy||" + varyBy;

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Theory]
        [InlineData("Cookie0", "CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value)")]
        [InlineData("Cookie0,Cookie1",
            "CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
        [InlineData("Cookie0, Cookie1",
            "CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
        [InlineData("   Cookie0,   ,   Cookie1   ",
            "CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
        [InlineData(",Cookie0,,Cookie1,",
            "CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
        public void GenerateKey_UsesVaryByCookieName(string varyByCookie, string expected)
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByCookie = varyByCookie
            };
            cacheTagHelper.ViewContext.HttpContext.Request.Headers["Cookie"] =
                "Cookie0=Cookie0Value;Cookie1=Cookie1Value";

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Theory]
        [InlineData("Accept-Language", "CacheTagHelper||testid||VaryByHeader(Accept-Language||en-us;charset=utf8)")]
        [InlineData("X-CustomHeader,Accept-Encoding, NotAvailable",
            "CacheTagHelper||testid||VaryByHeader(X-CustomHeader||Header-Value||Accept-Encoding||utf8||NotAvailable||)")]
        [InlineData("X-CustomHeader,  , Accept-Encoding, NotAvailable",
            "CacheTagHelper||testid||VaryByHeader(X-CustomHeader||Header-Value||Accept-Encoding||utf8||NotAvailable||)")]
        public void GenerateKey_UsesVaryByHeader(string varyByHeader, string expected)
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByHeader = varyByHeader
            };
            var headers = cacheTagHelper.ViewContext.HttpContext.Request.Headers;
            headers["Accept-Language"] = "en-us;charset=utf8";
            headers["Accept-Encoding"] = "utf8";
            headers["X-CustomHeader"] = "Header-Value";

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Theory]
        [InlineData("category", "CacheTagHelper||testid||VaryByQuery(category||cats)")]
        [InlineData("Category,SortOrder,SortOption",
            "CacheTagHelper||testid||VaryByQuery(Category||cats||SortOrder||||SortOption||Adorability)")]
        [InlineData("Category,  SortOrder, SortOption,  ",
            "CacheTagHelper||testid||VaryByQuery(Category||cats||SortOrder||||SortOption||Adorability)")]
        public void GenerateKey_UsesVaryByQuery(string varyByQuery, string expected)
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByQuery = varyByQuery
            };
            cacheTagHelper.ViewContext.HttpContext.Request.QueryString =
                new QueryString("?sortoption=Adorability&Category=cats&sortOrder=");

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Theory]
        [InlineData("id", "CacheTagHelper||testid||VaryByRoute(id||4)")]
        [InlineData("Category,,Id,OptionRouteValue",
            "CacheTagHelper||testid||VaryByRoute(Category||MyCategory||Id||4||OptionRouteValue||)")]
        [InlineData(" Category,  , Id,   OptionRouteValue,   ",
            "CacheTagHelper||testid||VaryByRoute(Category||MyCategory||Id||4||OptionRouteValue||)")]
        public void GenerateKey_UsesVaryByRoute(string varyByRoute, string expected)
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByRoute = varyByRoute
            };
            cacheTagHelper.ViewContext.RouteData.Values["id"] = 4;
            cacheTagHelper.ViewContext.RouteData.Values["category"] = "MyCategory";

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Fact]
        [ReplaceCulture("de-CH", "de-CH")]
        public void GenerateKey_UsesVaryByRoute_UsesInvariantCulture()
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(
                new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByRoute = "Category",
            };
            cacheTagHelper.ViewContext.RouteData.Values["id"] = 4;
            cacheTagHelper.ViewContext.RouteData.Values["category"] =
                new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7));
            var expected = "CacheTagHelper||testid||VaryByRoute(Category||10/31/2018 07:37:38 -07:00)";

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Fact]
        public void GenerateKey_UsesVaryByUser_WhenUserIsNotAuthenticated()
        {
            // Arrange
            var expected = "CacheTagHelper||testid||VaryByUser||";
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByUser = true
            };

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Fact]
        public void GenerateKey_UsesVaryByUserAndAuthenticatedUserName()
        {
            // Arrange
            var expected = "CacheTagHelper||testid||VaryByUser||test_name";
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByUser = true
            };
            var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "test_name") });
            cacheTagHelper.ViewContext.HttpContext.User = new ClaimsPrincipal(identity);

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Fact]
        [ReplaceCulture("fr-FR", "es-ES")]
        public void GenerateKey_UsesCultureAndUICultureName_IfVaryByCulture_IsSet()
        {
            // Arrange
            var expected = "CacheTagHelper||testid||VaryByCulture||fr-FR||es-ES";
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByCulture = true
            };

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Fact]
        public void GenerateKey_WithMultipleVaryByOptions_CreatesCombinedKey()
        {
            // Arrange
            var expected = "CacheTagHelper||testid||VaryBy||custom-value||" +
                "VaryByHeader(content-type||text/html)||VaryByUser||someuser";
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByUser = true,
                VaryByHeader = "content-type",
                VaryBy = "custom-value"
            };
            cacheTagHelper.ViewContext.HttpContext.Request.Headers["Content-Type"] = "text/html";
            var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "someuser") });
            cacheTagHelper.ViewContext.HttpContext.User = new ClaimsPrincipal(identity);

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Fact]
        [ReplaceCulture("zh", "zh-Hans")]
        public void GenerateKey_WithVaryByCulture_ComposesWithOtherOptions()
        {
            // Arrange
            var expected = "CacheTagHelper||testid||VaryBy||custom-value||" +
                "VaryByHeader(content-type||text/html)||VaryByCulture||zh||zh-Hans";
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByCulture = true,
                VaryByHeader = "content-type",
                VaryBy = "custom-value"
            };
            cacheTagHelper.ViewContext.HttpContext.Request.Headers["Content-Type"] = "text/html";

            // Act
            var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
            var key = cacheTagKey.GenerateKey();

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

        [Fact]
        public void Equality_ReturnsFalse_WhenVaryByCultureIsTrue_AndCultureIsDifferent()
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByCulture = true,
            };

            // Act
            CacheTagKey key1;
            using (new CultureReplacer("fr-FR"))
            {
                key1 = new CacheTagKey(cacheTagHelper, tagHelperContext);
            }

            CacheTagKey key2;
            using (new CultureReplacer("es-ES"))
            {
                key2 = new CacheTagKey(cacheTagHelper, tagHelperContext);
            }
            var equals = key1.Equals(key2);
            var hashCode1 = key1.GetHashCode();
            var hashCode2 = key2.GetHashCode();

            // Assert
            Assert.False(equals, "CacheTagKeys must not be equal");
            Assert.NotEqual(hashCode1, hashCode2);
        }

        [Fact]
        public void Equality_ReturnsFalse_WhenVaryByCultureIsTrue_AndUICultureIsDifferent()
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByCulture = true,
            };

            // Act
            CacheTagKey key1;
            using (new CultureReplacer("fr", "fr-FR"))
            {
                key1 = new CacheTagKey(cacheTagHelper, tagHelperContext);
            }

            CacheTagKey key2;
            using (new CultureReplacer("fr", "fr-CA"))
            {
                key2 = new CacheTagKey(cacheTagHelper, tagHelperContext);
            }
            var equals = key1.Equals(key2);
            var hashCode1 = key1.GetHashCode();
            var hashCode2 = key2.GetHashCode();

            // Assert
            Assert.False(equals, "CacheTagKeys must not be equal");
            Assert.NotEqual(hashCode1, hashCode2);
        }

        [Fact]
        public void Equality_ReturnsTrue_WhenVaryByCultureIsTrue_AndCultureIsSame()
        {
            // Arrange
            var tagHelperContext = GetTagHelperContext();
            var cacheTagHelper = new CacheTagHelper(new CacheTagHelperMemoryCacheFactory(Mock.Of<IMemoryCache>()), new HtmlTestEncoder())
            {
                ViewContext = GetViewContext(),
                VaryByCulture = true,
            };

            // Act
            CacheTagKey key1;
            CacheTagKey key2;
            using (new CultureReplacer("fr-FR", "fr-FR"))
            {
                key1 = new CacheTagKey(cacheTagHelper, tagHelperContext);
            }

            using (new CultureReplacer("fr-fr", "fr-fr"))
            {
                key2 = new CacheTagKey(cacheTagHelper, tagHelperContext);
            }

            var equals = key1.Equals(key2);
            var hashCode1 = key1.GetHashCode();
            var hashCode2 = key2.GetHashCode();

            // Assert
            Assert.True(equals, "CacheTagKeys must be equal");
            Assert.Equal(hashCode1, hashCode2);
        }

        private static ViewContext GetViewContext()
        {
            var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
            return new ViewContext(actionContext,
                Mock.Of<IView>(),
                new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()),
                Mock.Of<ITempDataDictionary>(),
                TextWriter.Null,
                new HtmlHelperOptions());
        }

        private static TagHelperContext GetTagHelperContext(string id = "testid")
        {
            return new TagHelperContext(
                tagName: "test",
                allAttributes: new TagHelperAttributeList(),
                items: new Dictionary<object, object>(),
                uniqueId: id);
        }
    }
}
