// 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.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
{
    public class AuthorizationMessageHandlerTests
    {
        [Fact]
        public async Task Throws_IfTheListOfAllowedUrlsIsNotConfigured()
        {
            // Arrange
            var expectedMessage = "The 'AuthorizationMessageHandler' is not configured. " +
                    "Call 'ConfigureHandler' and provide a list of endpoint urls to attach the token to.";

            var tokenProvider = new Mock<IAccessTokenProvider>();

            var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
            // Act & Assert

            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
                () => new HttpClient(handler).GetAsync("https://www.example.com"));

            Assert.Equal(expectedMessage, exception.Message);
        }

        [Fact]
        public async Task DoesNotAttachTokenToRequest_IfNotPresentInListOfAllowedUrls()
        {
            // Arrange
            var tokenProvider = new Mock<IAccessTokenProvider>();

            var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
            handler.ConfigureHandler(new[] { "https://localhost:5001" });

            var response = new HttpResponseMessage(HttpStatusCode.OK);
            handler.InnerHandler = new TestMessageHandler(response);

            // Act
            _ = await new HttpClient(handler).GetAsync("https://www.example.com");

            // Assert
            tokenProvider.VerifyNoOtherCalls();
        }

        [Fact]
        public async Task RequestsTokenWithDefaultScopes_WhenNoTokenIsAvailable()
        {
            // Arrange
            var tokenProvider = new Mock<IAccessTokenProvider>();
            tokenProvider.Setup(tp => tp.RequestAccessToken())
                    .Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.Success,
                    new AccessToken
                    {
                        Expires = DateTime.Now.AddHours(1),
                        GrantedScopes = new string[] { "All" },
                        Value = "asdf"
                    },
                    "https://www.example.com")));

            var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
            handler.ConfigureHandler(new[] { "https://localhost:5001" });

            var response = new HttpResponseMessage(HttpStatusCode.OK);
            handler.InnerHandler = new TestMessageHandler(response);

            // Act
            _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");

            // Assert
            Assert.Equal("asdf", response.RequestMessage.Headers.Authorization.Parameter);
        }

        [Fact]
        public async Task CachesExistingTokenWhenPossible()
        {
            // Arrange
            var tokenProvider = new Mock<IAccessTokenProvider>();
            tokenProvider.Setup(tp => tp.RequestAccessToken())
                    .Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.Success,
                    new AccessToken
                    {
                        Expires = DateTime.Now.AddHours(1),
                        GrantedScopes = new string[] { "All" },
                        Value = "asdf"
                    },
                    "https://www.example.com")));

            var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
            handler.ConfigureHandler(new[] { "https://localhost:5001" });

            var response = new HttpResponseMessage(HttpStatusCode.OK);
            handler.InnerHandler = new TestMessageHandler(response);

            // Act
            _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");
            response.RequestMessage = null;

            _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");

            // Assert
            Assert.Single(tokenProvider.Invocations);
            Assert.Equal("asdf", response.RequestMessage.Headers.Authorization.Parameter);
        }

        [Fact]
        public async Task RequestNewTokenWhenCurrentTokenIsAboutToExpire()
        {
            // Arrange
            var tokenProvider = new Mock<IAccessTokenProvider>();
            tokenProvider.Setup(tp => tp.RequestAccessToken())
                    .Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.Success,
                    new AccessToken
                    {
                        Expires = DateTime.Now.AddMinutes(3),
                        GrantedScopes = new string[] { "All" },
                        Value = "asdf"
                    },
                    "https://www.example.com")));

            var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
            handler.ConfigureHandler(new[] { "https://localhost:5001" });

            var response = new HttpResponseMessage(HttpStatusCode.OK);
            handler.InnerHandler = new TestMessageHandler(response);

            // Act
            _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");
            response.RequestMessage = null;

            _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");

            // Assert
            Assert.Equal(2, tokenProvider.Invocations.Count);
        }

        [Fact]
        public async Task ThrowsWhenItCanNotProvisionANewToken()
        {
            // Arrange
            var tokenProvider = new Mock<IAccessTokenProvider>();
            tokenProvider.Setup(tp => tp.RequestAccessToken())
                    .Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.RequiresRedirect,
                    null,
                    "https://www.example.com")));

            var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
            handler.ConfigureHandler(new[] { "https://localhost:5001" });

            var response = new HttpResponseMessage(HttpStatusCode.OK);
            handler.InnerHandler = new TestMessageHandler(response);

            // Act & assert
            var exception = await Assert.ThrowsAsync<AccessTokenNotAvailableException>(() => new HttpClient(handler).GetAsync("https://localhost:5001/weather"));
        }

        [Fact]
        public async Task UsesCustomScopesAndReturnUrlWhenProvided()
        {
            // Arrange
            var tokenProvider = new Mock<IAccessTokenProvider>();
            tokenProvider.Setup(tp => tp.RequestAccessToken(It.IsAny<AccessTokenRequestOptions>()))
                .Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.Success,
                new AccessToken
                {
                    Expires = DateTime.Now.AddMinutes(3),
                    GrantedScopes = new string[] { "All" },
                    Value = "asdf"
                },
                "https://www.example.com/return")));

            var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
            handler.ConfigureHandler(
                new[] { "https://localhost:5001" },
                scopes: new[] { "example.read", "example.write" },
                returnUrl: "https://www.example.com/return");

            var response = new HttpResponseMessage(HttpStatusCode.OK);
            handler.InnerHandler = new TestMessageHandler(response);

            // Act
            _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");

            // Assert
            Assert.Equal(1, tokenProvider.Invocations.Count);
        }
    }

    internal class TestMessageHandler : HttpMessageHandler
    {
        private readonly HttpResponseMessage _response;

        public TestMessageHandler(HttpResponseMessage response) => _response = response;

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            _response.RequestMessage = request;
            return Task.FromResult(_response);
        }
    }
}
