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

using System.Security.Claims;
using System.Security.Principal;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.CookiePolicy.Test;

public class CookiePolicyTests
{
    private readonly RequestDelegate SecureCookieAppends = context =>
    {
        context.Response.Cookies.Append("A", "A");
        context.Response.Cookies.Append("B", "B", new CookieOptions { Secure = false });
        context.Response.Cookies.Append("C", "C", new CookieOptions());
        context.Response.Cookies.Append("D", "D", new CookieOptions { Secure = true });
        return Task.FromResult(0);
    };
    private readonly RequestDelegate HttpCookieAppends = context =>
    {
        context.Response.Cookies.Append("A", "A");
        context.Response.Cookies.Append("B", "B", new CookieOptions { HttpOnly = false });
        context.Response.Cookies.Append("C", "C", new CookieOptions());
        context.Response.Cookies.Append("D", "D", new CookieOptions { HttpOnly = true });
        return Task.FromResult(0);
    };
    private readonly RequestDelegate SameSiteCookieAppends = context =>
    {
        context.Response.Cookies.Append("A", "A");
        context.Response.Cookies.Append("B", "B", new CookieOptions());
        context.Response.Cookies.Append("C", "C", new CookieOptions { SameSite = Http.SameSiteMode.None });
        context.Response.Cookies.Append("D", "D", new CookieOptions { SameSite = Http.SameSiteMode.Lax });
        context.Response.Cookies.Append("E", "E", new CookieOptions { SameSite = Http.SameSiteMode.Strict });
        context.Response.Cookies.Append("F", "F", new CookieOptions { SameSite = (Http.SameSiteMode)(-1) });
        return Task.FromResult(0);
    };

    [Fact]
    public async Task SecureAlwaysSetsSecure()
    {
        await RunTest("/secureAlways",
            new CookiePolicyOptions
            {
                Secure = CookieSecurePolicy.Always
            },
            SecureCookieAppends,
            new RequestTest("http://example.com/secureAlways",
                transaction =>
                {
                    Assert.NotNull(transaction.SetCookie);
                    Assert.Equal("A=A; path=/; secure", transaction.SetCookie[0].AsSpan());
                    Assert.Equal("B=B; path=/; secure", transaction.SetCookie[1].AsSpan());
                    Assert.Equal("C=C; path=/; secure", transaction.SetCookie[2].AsSpan());
                    Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3].AsSpan());
                }));
    }

    [Fact]
    public async Task SecureNoneLeavesSecureUnchanged()
    {
        await RunTest("/secureNone",
            new CookiePolicyOptions
            {
                Secure = CookieSecurePolicy.None
            },
            SecureCookieAppends,
            new RequestTest("http://example.com/secureNone",
                transaction =>
                {
                    Assert.NotNull(transaction.SetCookie);
                    Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
                    Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
                    Assert.Equal("C=C; path=/", transaction.SetCookie[2]);
                    Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]);
                }));
    }

    [Fact]
    public async Task SecureSameUsesRequest()
    {
        await RunTest("/secureSame",
            new CookiePolicyOptions
            {
                Secure = CookieSecurePolicy.SameAsRequest
            },
            SecureCookieAppends,
            new RequestTest("http://example.com/secureSame",
                transaction =>
                {
                    Assert.NotNull(transaction.SetCookie);
                    Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
                    Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
                    Assert.Equal("C=C; path=/", transaction.SetCookie[2]);
                    Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]);
                }),
            new RequestTest("https://example.com/secureSame",
                transaction =>
                {
                    Assert.NotNull(transaction.SetCookie);
                    Assert.Equal("A=A; path=/; secure", transaction.SetCookie[0]);
                    Assert.Equal("B=B; path=/; secure", transaction.SetCookie[1]);
                    Assert.Equal("C=C; path=/; secure", transaction.SetCookie[2]);
                    Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]);
                }));
    }

    [Fact]
    public async Task HttpOnlyAlwaysSetsItAlways()
    {
        await RunTest("/httpOnlyAlways",
            new CookiePolicyOptions
            {
                HttpOnly = HttpOnlyPolicy.Always
            },
            HttpCookieAppends,
            new RequestTest("http://example.com/httpOnlyAlways",
            transaction =>
            {
                Assert.NotNull(transaction.SetCookie);
                Assert.Equal("A=A; path=/; httponly", transaction.SetCookie[0]);
                Assert.Equal("B=B; path=/; httponly", transaction.SetCookie[1]);
                Assert.Equal("C=C; path=/; httponly", transaction.SetCookie[2]);
                Assert.Equal("D=D; path=/; httponly", transaction.SetCookie[3]);
            }));
    }

    [Fact]
    public async Task HttpOnlyNoneLeavesItAlone()
    {
        await RunTest("/httpOnlyNone",
            new CookiePolicyOptions
            {
                HttpOnly = HttpOnlyPolicy.None
            },
            HttpCookieAppends,
            new RequestTest("http://example.com/httpOnlyNone",
            transaction =>
            {
                Assert.NotNull(transaction.SetCookie);
                Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
                Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
                Assert.Equal("C=C; path=/", transaction.SetCookie[2]);
                Assert.Equal("D=D; path=/; httponly", transaction.SetCookie[3]);
            }));
    }

    [Fact]
    public async Task SameSiteStrictSetsItAlways()
    {
        await RunTest("/sameSiteStrict",
            new CookiePolicyOptions
            {
                MinimumSameSitePolicy = Http.SameSiteMode.Strict
            },
            SameSiteCookieAppends,
            new RequestTest("http://example.com/sameSiteStrict",
            transaction =>
            {
                Assert.NotNull(transaction.SetCookie);
                Assert.Equal("A=A; path=/; samesite=strict", transaction.SetCookie[0]);
                Assert.Equal("B=B; path=/; samesite=strict", transaction.SetCookie[1]);
                Assert.Equal("C=C; path=/; samesite=strict", transaction.SetCookie[2]);
                Assert.Equal("D=D; path=/; samesite=strict", transaction.SetCookie[3]);
                Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
            }));
    }

    [Fact]
    public async Task SameSiteLaxSetsItAlways()
    {
        await RunTest("/sameSiteLax",
            new CookiePolicyOptions
            {
                MinimumSameSitePolicy = Http.SameSiteMode.Lax
            },
            SameSiteCookieAppends,
            new RequestTest("http://example.com/sameSiteLax",
            transaction =>
            {
                Assert.NotNull(transaction.SetCookie);
                Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
                Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]);
                Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
                Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
                Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
            }));
    }

    [Fact]
    public async Task SameSiteNoneSetsItAlways()
    {
        await RunTest("/sameSiteNone",
            new CookiePolicyOptions
            {
                MinimumSameSitePolicy = Http.SameSiteMode.None
            },
            SameSiteCookieAppends,
            new RequestTest("http://example.com/sameSiteNone",
            transaction =>
            {
                Assert.NotNull(transaction.SetCookie);
                Assert.Equal("A=A; path=/; samesite=none", transaction.SetCookie[0]);
                Assert.Equal("B=B; path=/; samesite=none", transaction.SetCookie[1]);
                Assert.Equal("C=C; path=/; samesite=none", transaction.SetCookie[2]);
                Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
                Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
            }));
    }

    [Fact]
    public async Task SameSiteUnspecifiedLeavesItAlone()
    {
        await RunTest("/sameSiteNone",
            new CookiePolicyOptions
            {
                MinimumSameSitePolicy = Http.SameSiteMode.Unspecified
            },
            SameSiteCookieAppends,
            new RequestTest("http://example.com/sameSiteNone",
            transaction =>
            {
                Assert.NotNull(transaction.SetCookie);
                Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
                Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
                Assert.Equal("C=C; path=/; samesite=none", transaction.SetCookie[2]);
                Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
                Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
                Assert.Equal("F=F; path=/", transaction.SetCookie[5]);
            }));
    }

    [Fact]
    public async Task CookiePolicyCanHijackAppend()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                    .Configure(app =>
                    {
                        app.UseCookiePolicy(new CookiePolicyOptions
                        {
                            OnAppendCookie = ctx => ctx.CookieName = ctx.CookieValue = "Hao"
                        });
                        app.Run(context =>
                        {
                            context.Response.Cookies.Append("A", "A");
                            context.Response.Cookies.Append("B", "B", new CookieOptions { Secure = false });
                            context.Response.Cookies.Append("C", "C", new CookieOptions() { SameSite = Http.SameSiteMode.Strict });
                            context.Response.Cookies.Append("D", "D", new CookieOptions { Secure = true });
                            return Task.FromResult(0);
                        });
                    })
                    .UseTestServer();
            })
            .Build();

        var server = host.GetTestServer();

        await host.StartAsync();

        var transaction = await server.SendAsync("http://example.com/login");

        Assert.NotNull(transaction.SetCookie);
        Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[0]);
        Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[1]);
        Assert.Equal("Hao=Hao; path=/; samesite=strict", transaction.SetCookie[2]);
        Assert.Equal("Hao=Hao; path=/; secure", transaction.SetCookie[3]);
    }

    [Fact]
    public async Task CookiePolicyCanHijackDelete()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                    .Configure(app =>
                    {
                        app.UseCookiePolicy(new CookiePolicyOptions
                        {
                            OnDeleteCookie = ctx => ctx.CookieName = "A"
                        });
                        app.Run(context =>
                        {
                            context.Response.Cookies.Delete("A");
                            context.Response.Cookies.Delete("B", new CookieOptions { Secure = false });
                            context.Response.Cookies.Delete("C", new CookieOptions());
                            context.Response.Cookies.Delete("D", new CookieOptions { Secure = true });
                            return Task.FromResult(0);
                        });
                    })
                    .UseTestServer();
            })
            .Build();

        var server = host.GetTestServer();

        await host.StartAsync();

        var transaction = await server.SendAsync("http://example.com/login");

        Assert.NotNull(transaction.SetCookie);
        Assert.Single(transaction.SetCookie);
        Assert.Equal("A=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure", transaction.SetCookie[0]);
    }

    [Fact]
    public async Task CookiePolicyCallsCookieFeature()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                    .Configure(app =>
                    {
                        app.Use(next => context =>
                        {
                            context.Features.Set<IResponseCookiesFeature>(new TestCookieFeature());
                            return next(context);
                        });
                        app.UseCookiePolicy(new CookiePolicyOptions
                        {
                            OnDeleteCookie = ctx => ctx.CookieName = "A"
                        });
                        app.Run(context =>
                        {
                            Assert.Throws<NotImplementedException>(() => context.Response.Cookies.Delete("A"));
                            Assert.Throws<NotImplementedException>(() => context.Response.Cookies.Delete("A", new CookieOptions()));
                            Assert.Throws<NotImplementedException>(() => context.Response.Cookies.Append("A", "A"));
                            Assert.Throws<NotImplementedException>(() => context.Response.Cookies.Append("A", "A", new CookieOptions()));
                            return context.Response.WriteAsync("Done");
                        });
                    })
                    .UseTestServer();
            })
            .Build();

        var server = host.GetTestServer();

        await host.StartAsync();

        var transaction = await server.SendAsync("http://example.com/login");
        Assert.Equal("Done", transaction.ResponseText);
    }

    [Fact]
    public async Task CookiePolicyAppliesToCookieAuth()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                    .Configure(app =>
                    {
                        app.UseCookiePolicy(new CookiePolicyOptions
                        {
                            HttpOnly = HttpOnlyPolicy.Always,
                            Secure = CookieSecurePolicy.Always,
                            OnAppendCookie = c => c.CookieOptions.Extensions.Add("extension")
                        });
                        app.UseAuthentication();
                        app.Run(context =>
                        {
                            return context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("TestUser", "Cookies"))));
                        });
                    })
                    .UseTestServer();
            })
            .ConfigureServices(services =>
            {
                services.AddAuthentication().AddCookie(o =>
                {
                    o.Cookie.Name = "TestCookie";
                    o.Cookie.HttpOnly = false;
                    o.Cookie.SecurePolicy = CookieSecurePolicy.None;
                });
            })
            .Build();

        var server = host.GetTestServer();

        await host.StartAsync();

        var transaction = await server.SendAsync("http://example.com/login");

        Assert.NotNull(transaction.SetCookie);
        Assert.Single(transaction.SetCookie);
        var cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[0]);
        Assert.Equal("TestCookie", cookie.Name.AsSpan());
        Assert.True(cookie.HttpOnly);
        Assert.True(cookie.Secure);
        Assert.Equal("/", cookie.Path.AsSpan());
        Assert.Contains("extension", cookie.Extensions);
    }

    [Fact]
    public async Task CookiePolicyAppliesToCookieAuthChunks()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                    .Configure(app =>
                    {
                        app.UseCookiePolicy(new CookiePolicyOptions
                        {
                            HttpOnly = HttpOnlyPolicy.Always,
                            Secure = CookieSecurePolicy.Always,
                            OnAppendCookie = c => c.CookieOptions.Extensions.Add("ext")
                        });
                        app.UseAuthentication();
                        app.Run(context =>
                        {
                            return context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity(new string('c', 1024 * 5), "Cookies"))));
                        });
                    })
                    .UseTestServer();
            })
            .ConfigureServices(services =>
            {
                services.AddAuthentication().AddCookie(o =>
                {
                    o.Cookie.Name = "TestCookie";
                    o.Cookie.HttpOnly = false;
                    o.Cookie.SecurePolicy = CookieSecurePolicy.None;
                });
            })
            .Build();

        var server = host.GetTestServer();

        await host.StartAsync();

        var transaction = await server.SendAsync("http://example.com/login");

        Assert.NotNull(transaction.SetCookie);
        Assert.Equal(3, transaction.SetCookie.Count);

        var cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[0]);
        Assert.Equal("TestCookie", cookie.Name.AsSpan());
        Assert.Equal("chunks-2", cookie.Value.AsSpan());
        Assert.True(cookie.HttpOnly);
        Assert.True(cookie.Secure);
        Assert.Equal("/", cookie.Path.AsSpan());
        Assert.Contains("ext", cookie.Extensions);

        cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[1]);
        Assert.Equal("TestCookieC1", cookie.Name.AsSpan());
        Assert.True(cookie.HttpOnly);
        Assert.True(cookie.Secure);
        Assert.Equal("/", cookie.Path.AsSpan());
        Assert.Contains("ext", cookie.Extensions);

        cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[2]);
        Assert.Equal("TestCookieC2", cookie.Name.AsSpan());
        Assert.True(cookie.HttpOnly);
        Assert.True(cookie.Secure);
        Assert.Equal("/", cookie.Path.AsSpan());
        Assert.Contains("ext", cookie.Extensions);
    }

    private class TestCookieFeature : IResponseCookiesFeature
    {
        public IResponseCookies Cookies { get; } = new BadCookies();

        private class BadCookies : IResponseCookies
        {
            public void Append(string key, string value)
            {
                throw new NotImplementedException();
            }

            public void Append(string key, string value, CookieOptions options)
            {
                throw new NotImplementedException();
            }

            public void Delete(string key)
            {
                throw new NotImplementedException();
            }

            public void Delete(string key, CookieOptions options)
            {
                throw new NotImplementedException();
            }
        }
    }

    private class RequestTest
    {
        public RequestTest(string testUri, Action<Transaction> verify)
        {
            TestUri = testUri;
            Verification = verify;
        }

        public async Task Execute(TestServer server)
        {
            var transaction = await server.SendAsync(TestUri);
            Verification(transaction);
        }

        public string TestUri { get; set; }
        public Action<Transaction> Verification { get; set; }
    }

    private async Task RunTest(
        string path,
        CookiePolicyOptions cookiePolicy,
        RequestDelegate configureSetup,
        params RequestTest[] tests)
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                    .Configure(app =>
                    {
                        app.Map(path, map =>
                        {
                            map.UseCookiePolicy(cookiePolicy);
                            map.Run(configureSetup);
                        });
                    })
                    .UseTestServer();
            })
            .Build();

        var server = host.GetTestServer();

        await host.StartAsync();

        foreach (var test in tests)
        {
            await test.Execute(server);
        }
    }
}
