// 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.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using Xunit;

namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
{
    public class OpenIdConnectEventTests
    {
        private readonly RequestDelegate AppWritePath = context => context.Response.WriteAsync(context.Request.Path);
        private readonly RequestDelegate AppNotImpl = context => { throw new NotImplementedException("App"); };

        [Fact]
        public async Task OnMessageReceived_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
            };
            events.OnMessageReceived = context =>
            {
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnMessageReceived_Fail_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectRemoteFailure = true,
            };
            events.OnMessageReceived = context =>
            {
                context.Fail("Authentication was aborted from user code.");
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var exception = await Assert.ThrowsAsync<Exception>(delegate
            {
                return PostAsync(server, "signin-oidc", "");
            });

            Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnMessageReceived_Handled_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
            };
            events.OnMessageReceived = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenValidated_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
            };
            events.OnTokenValidated = context =>
            {
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenValidated_Fail_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectRemoteFailure = true,
            };
            events.OnTokenValidated = context =>
            {
                context.Fail("Authentication was aborted from user code.");
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var exception = await Assert.ThrowsAsync<Exception>(delegate
            {
                return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state");
            });

            Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenValidated_HandledWithoutTicket_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
            };
            events.OnTokenValidated = context =>
            {
                context.HandleResponse();
                context.Principal = null;
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenValidated_HandledWithTicket_SkipToTicketReceived()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectTicketReceived = true,
            };
            events.OnTokenValidated = context =>
            {
                context.HandleResponse();
                context.Principal = null;
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            events.OnTokenValidated = context =>
            {
                context.Success();
                return Task.FromResult(0);
            };
            events.OnTicketReceived = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAuthorizationCodeReceived_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
            };
            events.OnAuthorizationCodeReceived = context =>
            {
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAuthorizationCodeReceived_Fail_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectRemoteFailure = true,
            };
            events.OnAuthorizationCodeReceived = context =>
            {
                context.Fail("Authentication was aborted from user code.");
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var exception = await Assert.ThrowsAsync<Exception>(delegate
            {
                return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
            });

            Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAuthorizationCodeReceived_HandledWithoutTicket_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
            };
            events.OnAuthorizationCodeReceived = context =>
            {
                context.HandleResponse();
                context.Principal = null;
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAuthorizationCodeReceived_HandledWithTicket_SkipToTicketReceived()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTicketReceived = true,
            };
            events.OnAuthorizationCodeReceived = context =>
            {
                context.Success();
                return Task.FromResult(0);
            };
            events.OnTicketReceived = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenResponseReceived_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
            };
            events.OnTokenResponseReceived = context =>
            {
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenResponseReceived_Fail_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectRemoteFailure = true,
            };
            events.OnTokenResponseReceived = context =>
            {
                context.Fail("Authentication was aborted from user code.");
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var exception = await Assert.ThrowsAsync<Exception>(delegate
            {
                return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
            });

            Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenResponseReceived_HandledWithoutTicket_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
            };
            events.OnTokenResponseReceived = context =>
            {
                context.Principal = null;
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenResponseReceived_HandledWithTicket_SkipToTicketReceived()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectTicketReceived = true,
            };
            events.OnTokenResponseReceived = context =>
            {
                context.Success();
                return Task.FromResult(0);
            };
            events.OnTicketReceived = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenValidatedBackchannel_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
            };
            events.OnTokenValidated = context =>
            {
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenValidatedBackchannel_Fail_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectRemoteFailure = true,
            };
            events.OnTokenValidated = context =>
            {
                context.Fail("Authentication was aborted from user code.");
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var exception = await Assert.ThrowsAsync<Exception>(delegate
            {
                return PostAsync(server, "signin-oidc", "state=protected_state&code=my_code");
            });

            Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenValidatedBackchannel_HandledWithoutTicket_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
            };
            events.OnTokenValidated = context =>
            {
                context.Principal = null;
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTokenValidatedBackchannel_HandledWithTicket_SkipToTicketReceived()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectTicketReceived = true,
            };
            events.OnTokenValidated = context =>
            {
                context.Success();
                return Task.FromResult(0);
            };
            events.OnTicketReceived = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnUserInformationReceived_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
            };
            events.OnUserInformationReceived = context =>
            {
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnUserInformationReceived_Fail_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectRemoteFailure = true,
            };
            events.OnUserInformationReceived = context =>
            {
                context.Fail("Authentication was aborted from user code.");
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var exception = await Assert.ThrowsAsync<Exception>(delegate
            {
                return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
            });

            Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnUserInformationReceived_HandledWithoutTicket_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
            };
            events.OnUserInformationReceived = context =>
            {
                context.Principal = null;
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnUserInformationReceived_HandledWithTicket_SkipToTicketReceived()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectTicketReceived = true,
            };
            events.OnUserInformationReceived = context =>
            {
                context.Success();
                return Task.FromResult(0);
            };
            events.OnTicketReceived = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAuthenticationFailed_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectAuthenticationFailed = true,
            };
            events.OnUserInformationReceived = context =>
            {
                throw new NotImplementedException("TestException");
            };
            events.OnAuthenticationFailed = context =>
            {
                Assert.Equal("TestException", context.Exception.Message);
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAuthenticationFailed_Fail_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectAuthenticationFailed = true,
                ExpectRemoteFailure = true,
            };
            events.OnUserInformationReceived = context =>
            {
                throw new NotImplementedException("TestException");
            };
            events.OnAuthenticationFailed = context =>
            {
                Assert.Equal("TestException", context.Exception.Message);
                context.Fail("Authentication was aborted from user code.");
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var exception = await Assert.ThrowsAsync<Exception>(delegate
            {
                return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
            });

            Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAuthenticationFailed_HandledWithoutTicket_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectAuthenticationFailed = true,
            };
            events.OnUserInformationReceived = context =>
            {
                throw new NotImplementedException("TestException");
            };
            events.OnAuthenticationFailed = context =>
            {
                Assert.Equal("TestException", context.Exception.Message);
                Assert.Null(context.Principal);
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAuthenticationFailed_HandledWithTicket_SkipToTicketReceived()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectAuthenticationFailed = true,
                ExpectTicketReceived = true,
            };
            events.OnUserInformationReceived = context =>
            {
                throw new NotImplementedException("TestException");
            };
            events.OnAuthenticationFailed = context =>
            {
                Assert.Equal("TestException", context.Exception.Message);
                Assert.Null(context.Principal);

                var claims = new[]
                {
                    new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"),
                    new Claim(ClaimTypes.Email, "bob@contoso.com"),
                    new Claim(ClaimsIdentity.DefaultNameClaimType, "bob")
                };

                context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                context.Success();
                return Task.FromResult(0);
            };
            events.OnTicketReceived = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAccessDenied_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectAccessDenied = true
            };
            events.OnAccessDenied = context =>
            {
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "error=access_denied&state=protected_state");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnAccessDenied_Handled_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectAccessDenied = true
            };
            events.OnAccessDenied = context =>
            {
                Assert.Equal("testvalue", context.Properties.Items["testkey"]);
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "error=access_denied&state=protected_state");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnRemoteFailure_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectAuthenticationFailed = true,
                ExpectRemoteFailure = true,
            };
            events.OnUserInformationReceived = context =>
            {
                throw new NotImplementedException("TestException");
            };
            events.OnAuthenticationFailed = context =>
            {
                Assert.Equal("TestException", context.Exception.Message);
                return Task.FromResult(0);
            };
            events.OnRemoteFailure = context =>
            {
                Assert.Equal("TestException", context.Failure.Message);
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnRemoteFailure_Handled_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectAuthenticationFailed = true,
                ExpectRemoteFailure = true,
            };
            events.OnUserInformationReceived = context =>
            {
                throw new NotImplementedException("TestException");
            };
            events.OnRemoteFailure = context =>
            {
                Assert.Equal("TestException", context.Failure.Message);
                Assert.Equal("testvalue", context.Properties.Items["testkey"]);
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTicketReceived_Skip_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectTicketReceived = true,
            };
            events.OnTicketReceived = context =>
            {
                context.SkipHandler();
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppWritePath);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnTicketReceived_Handled_NoMoreEventsRun()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectMessageReceived = true,
                ExpectTokenValidated = true,
                ExpectAuthorizationCodeReceived = true,
                ExpectTokenResponseReceived = true,
                ExpectUserInfoReceived = true,
                ExpectTicketReceived = true,
            };
            events.OnTicketReceived = context =>
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.FromResult(0);
            };
            var server = CreateServer(events, AppNotImpl);

            var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Equal("", await response.Content.ReadAsStringAsync());
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnRedirectToIdentityProviderForSignOut_Invoked()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectRedirectForSignOut = true,
            };
            var server = CreateServer(events,
            context =>
            {
                return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
            });

            var client = server.CreateClient();
            var response = await client.GetAsync("/");

            Assert.Equal(HttpStatusCode.Found, response.StatusCode);
            Assert.Equal("http://testhost/end", response.Headers.Location.GetLeftPart(UriPartial.Path));
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnRedirectToIdentityProviderForSignOut_Handled_RedirectNotInvoked()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectRedirectForSignOut = true,
            };
            events.OnRedirectToIdentityProviderForSignOut = context =>
            {
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                context.HandleResponse();
                return Task.CompletedTask;
            };
            var server = CreateServer(events,
            context =>
            {
                return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
            });

            var client = server.CreateClient();
            var response = await client.GetAsync("/");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Null(response.Headers.Location);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnRemoteSignOut_Invoked()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectRemoteSignOut = true,
            };
            var server = CreateServer(events, AppNotImpl);

            var client = server.CreateClient();
            var response = await client.GetAsync("/signout-oidc");

            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            events.ValidateExpectations();
            Assert.True(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values));
            Assert.True(SetCookieHeaderValue.TryParseStrictList(values.ToList(), out var parsedValues));
            Assert.Equal(1, parsedValues.Count);
            Assert.True(StringSegment.IsNullOrEmpty(parsedValues.Single().Value));
        }

        [Fact]
        public async Task OnRemoteSignOut_Handled_NoSignout()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectRemoteSignOut = true,
            };
            events.OnRemoteSignOut = context =>
            {
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                context.HandleResponse();
                return Task.CompletedTask;
            };
            var server = CreateServer(events, AppNotImpl);

            var client = server.CreateClient();
            var response = await client.GetAsync("/signout-oidc");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            events.ValidateExpectations();
            Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values));
        }

        [Fact]
        public async Task OnRemoteSignOut_Skip_NoSignout()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectRemoteSignOut = true,
            };
            events.OnRemoteSignOut = context =>
            {
                context.SkipHandler();
                return Task.CompletedTask;
            };
            var server = CreateServer(events, context =>
            {
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.CompletedTask;
            });

            var client = server.CreateClient();
            var response = await client.GetAsync("/signout-oidc");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            events.ValidateExpectations();
            Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values));
        }

        [Fact]
        public async Task OnRedirectToSignedOutRedirectUri_Invoked()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectRedirectToSignedOut = true,
            };
            var server = CreateServer(events, AppNotImpl);

            var client = server.CreateClient();
            var response = await client.GetAsync("/signout-callback-oidc?state=protected_state");

            Assert.Equal(HttpStatusCode.Found, response.StatusCode);
            Assert.Equal("http://testhost/redirect", response.Headers.Location.AbsoluteUri);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnRedirectToSignedOutRedirectUri_Handled_NoRedirect()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectRedirectToSignedOut = true,
            };
            events.OnSignedOutCallbackRedirect = context =>
            {
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                context.HandleResponse();
                return Task.CompletedTask;
            };
            var server = CreateServer(events, AppNotImpl);

            var client = server.CreateClient();
            var response = await client.GetAsync("/signout-callback-oidc?state=protected_state");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Null(response.Headers.Location);
            events.ValidateExpectations();
        }

        [Fact]
        public async Task OnRedirectToSignedOutRedirectUri_Skipped_NoRedirect()
        {
            var events = new ExpectedOidcEvents()
            {
                ExpectRedirectToSignedOut = true,
            };
            events.OnSignedOutCallbackRedirect = context =>
            {
                context.SkipHandler();
                return Task.CompletedTask;
            };
            var server = CreateServer(events,
            context =>
            {
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                return Task.CompletedTask;
            });

            var client = server.CreateClient();
            var response = await client.GetAsync("/signout-callback-oidc?state=protected_state");

            Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
            Assert.Null(response.Headers.Location);
            events.ValidateExpectations();
        }

        private class ExpectedOidcEvents : OpenIdConnectEvents
        {
            public bool ExpectMessageReceived { get; set; }
            public bool InvokedMessageReceived { get; set; }

            public bool ExpectTokenValidated { get; set; }
            public bool InvokedTokenValidated { get; set; }

            public bool ExpectAccessDenied { get; set; }
            public bool InvokedAccessDenied { get; set; }

            public bool ExpectRemoteFailure { get; set; }
            public bool InvokedRemoteFailure { get; set; }

            public bool ExpectTicketReceived { get; set; }
            public bool InvokedTicketReceived { get; set; }

            public bool ExpectAuthorizationCodeReceived { get; set; }
            public bool InvokedAuthorizationCodeReceived { get; set; }

            public bool ExpectTokenResponseReceived { get; set; }
            public bool InvokedTokenResponseReceived { get; set; }

            public bool ExpectUserInfoReceived { get; set; }
            public bool InvokedUserInfoReceived { get; set; }

            public bool ExpectAuthenticationFailed { get; set; }
            public bool InvokeAuthenticationFailed { get; set; }

            public bool ExpectRedirectForSignOut { get; set; }
            public bool InvokedRedirectForSignOut { get; set; }

            public bool ExpectRemoteSignOut { get; set; }
            public bool InvokedRemoteSignOut { get; set; }

            public bool ExpectRedirectToSignedOut { get; set; }
            public bool InvokedRedirectToSignedOut { get; set; }

            public override Task MessageReceived(MessageReceivedContext context)
            {
                InvokedMessageReceived = true;
                return base.MessageReceived(context);
            }

            public override Task TokenValidated(TokenValidatedContext context)
            {
                InvokedTokenValidated = true;
                return base.TokenValidated(context);
            }

            public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
            {
                InvokedAuthorizationCodeReceived = true;
                return base.AuthorizationCodeReceived(context);
            }

            public override Task TokenResponseReceived(TokenResponseReceivedContext context)
            {
                InvokedTokenResponseReceived = true;
                return base.TokenResponseReceived(context);
            }

            public override Task UserInformationReceived(UserInformationReceivedContext context)
            {
                InvokedUserInfoReceived = true;
                return base.UserInformationReceived(context);
            }

            public override Task AuthenticationFailed(AuthenticationFailedContext context)
            {
                InvokeAuthenticationFailed = true;
                return base.AuthenticationFailed(context);
            }

            public override Task TicketReceived(TicketReceivedContext context)
            {
                InvokedTicketReceived = true;
                return base.TicketReceived(context);
            }

            public override Task AccessDenied(AccessDeniedContext context)
            {
                InvokedAccessDenied = true;
                return base.AccessDenied(context);
            }

            public override Task RemoteFailure(RemoteFailureContext context)
            {
                InvokedRemoteFailure = true;
                return base.RemoteFailure(context);
            }

            public override Task RedirectToIdentityProviderForSignOut(RedirectContext context)
            {
                InvokedRedirectForSignOut = true;
                return base.RedirectToIdentityProviderForSignOut(context);
            }

            public override Task RemoteSignOut(RemoteSignOutContext context)
            {
                InvokedRemoteSignOut = true;
                return base.RemoteSignOut(context);
            }

            public override Task SignedOutCallbackRedirect(RemoteSignOutContext context)
            {
                InvokedRedirectToSignedOut = true;
                return base.SignedOutCallbackRedirect(context);
            }

            public void ValidateExpectations()
            {
                Assert.Equal(ExpectMessageReceived, InvokedMessageReceived);
                Assert.Equal(ExpectTokenValidated, InvokedTokenValidated);
                Assert.Equal(ExpectAuthorizationCodeReceived, InvokedAuthorizationCodeReceived);
                Assert.Equal(ExpectTokenResponseReceived, InvokedTokenResponseReceived);
                Assert.Equal(ExpectUserInfoReceived, InvokedUserInfoReceived);
                Assert.Equal(ExpectAuthenticationFailed, InvokeAuthenticationFailed);
                Assert.Equal(ExpectTicketReceived, InvokedTicketReceived);
                Assert.Equal(ExpectAccessDenied, InvokedAccessDenied);
                Assert.Equal(ExpectRemoteFailure, InvokedRemoteFailure);
                Assert.Equal(ExpectRedirectForSignOut, InvokedRedirectForSignOut);
                Assert.Equal(ExpectRemoteSignOut, InvokedRemoteSignOut);
                Assert.Equal(ExpectRedirectToSignedOut, InvokedRedirectToSignedOut);
            }
        }

        private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appCode)
        {
            var builder = new WebHostBuilder()
                .ConfigureServices(services =>
                {
                    services.AddAuthentication(auth =>
                    {
                        auth.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                        auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
                    })
                        .AddCookie()
                        .AddOpenIdConnect(o =>
                    {
                        o.Events = events;
                        o.ClientId = "ClientId";
                        o.GetClaimsFromUserInfoEndpoint = true;
                        o.Configuration = new OpenIdConnectConfiguration()
                        {
                            TokenEndpoint = "http://testhost/tokens",
                            UserInfoEndpoint = "http://testhost/user",
                            EndSessionEndpoint = "http://testhost/end"
                        };
                        o.StateDataFormat = new TestStateDataFormat();
                        o.SecurityTokenValidator = new TestTokenValidator();
                        o.ProtocolValidator = new TestProtocolValidator();
                        o.BackchannelHttpHandler = new TestBackchannel();
                    });
                })
                .Configure(app =>
                {
                    app.UseAuthentication();
                    app.Run(appCode);
                });

            return new TestServer(builder);
        }

        private Task<HttpResponseMessage> PostAsync(TestServer server, string path, string form)
        {
            var client = server.CreateClient();
            var cookie = ".AspNetCore.Correlation." + OpenIdConnectDefaults.AuthenticationScheme + ".correlationId=N";
            client.DefaultRequestHeaders.Add("Cookie", cookie);
            return client.PostAsync("signin-oidc",
                new StringContent(form, Encoding.ASCII, "application/x-www-form-urlencoded"));
        }

        private class TestStateDataFormat : ISecureDataFormat<AuthenticationProperties>
        {
            private AuthenticationProperties Data { get; set; }

            public string Protect(AuthenticationProperties data)
            {
                return "protected_state";
            }

            public string Protect(AuthenticationProperties data, string purpose)
            {
                throw new NotImplementedException();
            }

            public AuthenticationProperties Unprotect(string protectedText)
            {
                Assert.Equal("protected_state", protectedText);
                var properties = new AuthenticationProperties(new Dictionary<string, string>()
                {
                    { ".xsrf", "correlationId" },
                    { OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, "redirect_uri" },
                    { "testkey", "testvalue" }
                });
                properties.RedirectUri = "http://testhost/redirect";
                return properties;
            }

            public AuthenticationProperties Unprotect(string protectedText, string purpose)
            {
                throw new NotImplementedException();
            }
        }

        private class TestTokenValidator : ISecurityTokenValidator
        {
            public bool CanValidateToken => true;

            public int MaximumTokenSizeInBytes
            {
                get { return 1024; }
                set { throw new NotImplementedException(); }
            }

            public bool CanReadToken(string securityToken)
            {
                Assert.Equal("my_id_token", securityToken);
                return true;
            }

            public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
            {
                Assert.Equal("my_id_token", securityToken);
                validatedToken = new JwtSecurityToken();
                return new ClaimsPrincipal(new ClaimsIdentity("customAuthType"));
            }
        }

        private class TestProtocolValidator : OpenIdConnectProtocolValidator
        {
            public override void ValidateAuthenticationResponse(OpenIdConnectProtocolValidationContext validationContext)
            {
            }

            public override void ValidateTokenResponse(OpenIdConnectProtocolValidationContext validationContext)
            {
            }

            public override void ValidateUserInfoResponse(OpenIdConnectProtocolValidationContext validationContext)
            {
            }
        }

        private class TestBackchannel : HttpMessageHandler
        {
            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
            {
                if (string.Equals("/tokens", request.RequestUri.AbsolutePath, StringComparison.Ordinal))
                {
                    return Task.FromResult(new HttpResponseMessage() { Content =
                       new StringContent("{ \"id_token\": \"my_id_token\", \"access_token\": \"my_access_token\" }", Encoding.ASCII, "application/json") });
                }
                if (string.Equals("/user", request.RequestUri.AbsolutePath, StringComparison.Ordinal))
                {
                    return Task.FromResult(new HttpResponseMessage() { Content = new StringContent("{ }", Encoding.ASCII, "application/json") });
                }

                throw new NotImplementedException(request.RequestUri.ToString());
            }
        }
    }
}
