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

using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Rewrite.IISUrlRewrite;
using Microsoft.AspNetCore.Rewrite.UrlActions;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;

namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite;

public class MiddlewareTests
{
    [Fact]
    public async Task Invoke_RedirectPathToPathAndQuery()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Rewrite to article.aspx"">
                <match url = ""^article/([0-9]+)/([_0-9a-z-]+)"" />
                <action type=""Redirect"" url =""article.aspx?id={R:1}&amp;title={R:2}"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
             .ConfigureWebHost(webHostBuilder =>
             {
                 webHostBuilder
                 .UseTestServer()
                 .Configure(app =>
                 {
                     app.UseRewriter(options);
                     app.Run(context => context.Response.WriteAsync(context.Response.Headers.Location));
                 });
             }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetAsync("article/10/hey");

        Assert.Equal("/article.aspx?id=10&title=hey", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task Invoke_RewritePathToPathAndQuery()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Rewrite to article.aspx"">
                <match url = ""^article/([0-9]+)/([_0-9a-z-]+)"" />
                <action type=""Rewrite"" url =""article.aspx?id={R:1}&amp;title={R:2}"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(context.Request.Path + context.Request.QueryString));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetStringAsync("/article/10/hey");

        Assert.Equal("/article.aspx?id=10&title=hey", response);
    }

    [Fact]
    public async Task Invoke_RewriteBasedOnQueryStringParameters()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Query String Rewrite"">
                <match url=""page\.asp$"" />
                <conditions>
                <add input=""{QUERY_STRING}"" pattern=""p1=(\d+)"" />
                <add input=""##{C:1}##_{QUERY_STRING}"" pattern=""##([^#]+)##_.*p2=(\d+)"" />
                </conditions>
                <action type=""Rewrite"" url=""newpage.aspx?param1={C:1}&amp;param2={C:2}"" appendQueryString=""false""/>
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(context.Request.Path + context.Request.QueryString));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetStringAsync("page.asp?p2=321&p1=123");

        Assert.Equal("/newpage.aspx?param1=123&param2=321", response);
    }

    [Fact]
    public async Task Invoke_RedirectToLowerCase()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Convert to lower case"" stopProcessing=""true"">
                <match url="".*[A-Z].*"" ignoreCase=""false"" />
                <action type=""Redirect"" url=""{ToLower:{R:0}}"" redirectType=""Permanent"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(context.Response.Headers.Location));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

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

        Assert.Equal("/hello", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task Invoke_RedirectRemoveTrailingSlash()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Remove trailing slash"" stopProcessing=""true"">
                <match url=""(.*)/$"" />
                <conditions>
                <add input=""{REQUEST_FILENAME}"" matchType=""IsFile"" negate=""true"" />
                <add input=""{REQUEST_FILENAME}"" matchType=""IsDirectory"" negate=""true"" />
                </conditions>
                <action type=""Redirect"" redirectType=""Permanent"" url=""{R:1}"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

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

        Assert.Equal("/hey/hello", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task Invoke_RedirectAddTrailingSlash()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Add trailing slash"" stopProcessing=""true"">
                <match url=""(.*[^/])$"" />
                <conditions>
                <add input=""{REQUEST_FILENAME}"" matchType=""IsFile"" negate=""true"" />
                <add input=""{REQUEST_FILENAME}"" matchType=""IsDirectory"" negate=""true"" />
                </conditions>
                <action type=""Redirect"" redirectType=""Permanent"" url=""{R:1}/"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

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

        Assert.Equal("/hey/hello/", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task Invoke_RedirectToHttps()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Redirect to HTTPS"" stopProcessing=""true"">
                <match url=""(.*)"" />
                <conditions>
                <add input=""{HTTPS}"" pattern=""^OFF$"" />
                </conditions>
                <action type=""Redirect"" url=""https://{HTTP_HOST}/{R:1}"" redirectType=""Permanent"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetAsync(new Uri("http://example.com"));

        Assert.Equal("https://example.com/", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task Invoke_RewriteToHttps()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Rewrite to HTTPS"" stopProcessing=""true"">
                <match url=""(.*)"" />
                <conditions>
                <add input=""{HTTPS}"" pattern=""^OFF$"" />
                </conditions>
                <action type=""Rewrite"" url=""https://{HTTP_HOST}/{R:1}"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(
                        context.Request.Scheme +
                        "://" +
                        context.Request.Host +
                        context.Request.Path +
                        context.Request.QueryString));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetStringAsync(new Uri("http://example.com"));

        Assert.Equal("https://example.com/", response);
    }

    [Fact]
    public async Task Invoke_ReverseProxyToAnotherSite()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Proxy"">
                <match url=""(.*)"" />
                <action type=""Rewrite"" url=""http://internalserver/{R:1}"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(
                        context.Request.Scheme +
                        "://" +
                        context.Request.Host +
                        context.Request.Path +
                        context.Request.QueryString));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetStringAsync(new Uri("http://example.com/"));

        Assert.Equal("http://internalserver/", response);
    }

    [Fact]
    public async Task Invoke_CaptureEmptyStringInRegexAssertRedirectLocationHasForwardSlash()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Test"">
                <match url=""(.*)"" />
                <action type=""Redirect"" url=""{R:1}"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(
                        context.Request.Scheme +
                        "://" +
                        context.Request.Host +
                        context.Request.Path +
                        context.Request.QueryString));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetAsync(new Uri("http://example.com/"));

        Assert.Equal("/", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task Invoke_CaptureEmptyStringInRegexAssertRewriteLocationHasForwardSlash()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Test"">
                <match url=""(.*)"" />
                <action type=""Rewrite"" url=""{R:1}"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(
                        context.Request.Path +
                        context.Request.QueryString));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetStringAsync(new Uri("http://example.com/"));

        Assert.Equal("/", response);
    }

    [Fact]
    public async Task Invoke_CaptureEmptyStringInRegexAssertLocationHeaderContainsPathBase()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Test"">
                <match url=""(.*)"" />
                <action type=""Redirect"" url=""{R:1}"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(
                            context.Request.Path +
                            context.Request.QueryString));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();
        server.BaseAddress = new Uri("http://localhost:5000/foo");

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

        Assert.Equal("/foo", response.Headers.Location.OriginalString);
    }

    [Theory]
    [InlineData("IsFile")]
    [InlineData("isfile")]
    [InlineData("IsDirectory")]
    [InlineData("isdirectory")]
    public async Task VerifyIsFileAndIsDirectoryParsing(string matchType)
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader($@"<rewrite>
                <rules>
                <rule name=""Test"">
                <match url=""(.*[^/])$"" />
                <conditions>
                <add input=""{{REQUEST_FILENAME}}"" matchType=""{matchType}"" negate=""true""/>
                </conditions>
                <action type=""Redirect"" url=""{{R:1}}/"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

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

        Assert.Equal("/hey/hello/", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task VerifyTrackAllCaptures()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Test"">
                <match url=""(.*)"" ignoreCase=""false"" />
                <conditions trackAllCaptures = ""true"" >
                <add input=""{REQUEST_URI}"" pattern=""^/([a-zA-Z]+)/([0-9]+)$"" />
                <add input=""{QUERY_STRING}"" pattern=""p2=([a-z]+)"" />
                </conditions>
                <action type=""Redirect"" url =""blogposts/{C:1}/{C:4}"" />
                <!--rewrite action uses back - references to both conditions -->
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetAsync("article/23?p1=123&p2=abc");

        Assert.Equal("/blogposts/article/abc?p1=123&p2=abc", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task VerifyTrackAllCapturesRuleAndConditionCapture()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Test"">
                <match url=""(.*)"" ignoreCase=""false"" />
                <conditions trackAllCaptures = ""true"" >
                <add input=""{REQUEST_URI}"" pattern=""^/([a-zA-Z]+)/([0-9]+)$"" />
                <add input=""{QUERY_STRING}"" pattern=""p2=([a-z]+)"" />
                </conditions>
                <action type=""Redirect"" url =""blog/{R:0}/{C:4}"" />
                <!--rewrite action uses back - references to both conditions -->
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetAsync("article/23?p1=123&p2=abc");

        Assert.Equal("/blog/article/23/abc?p1=123&p2=abc", response.Headers.Location.OriginalString);
    }

    [Fact]
    public async Task ThrowArgumentOutOfRangeExceptionWithCorrectMessage()
    {
        // Arrange, Act, Assert
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Test"">
                <match url=""(.*)"" ignoreCase=""false"" />
                <conditions trackAllCaptures = ""true"" >
                <add input=""{REQUEST_URI}"" pattern=""^/([a-zA-Z]+)/([0-9]+)$"" />
                <add input=""{QUERY_STRING}"" pattern=""p2=([a-z]+)"" />
                </conditions>
                <action type=""Redirect"" url =""blog/{R:0}/{C:9}"" />
                <!--rewrite action uses back - references to both conditions -->
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var ex = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => server.CreateClient().GetAsync("article/23?p1=123&p2=abc"));

        Assert.Equal("Cannot access back reference at index 9. Only 5 back references were captured.", ex.Message);
    }

    [Fact]
    public async Task Invoke_GlobalRuleConditionMatchesAgainstFullUri_ParsedRule()
    {
        // arrange
        var xml = @"<rewrite>
                            <globalRules>
                                <rule name=""Test"" patternSyntax=""ECMAScript"" stopProcessing=""true"">
                                    <match url="".*"" />
                                    <conditions logicalGrouping=""MatchAll"" trackAllCaptures=""false"">
                                        <add input=""{REQUEST_URI}"" pattern=""^http://localhost/([0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})(/.*)"" />
                                    </conditions>
                                    <action type=""Rewrite"" url=""http://www.test.com{C:2}"" />
                                </rule>
                            </globalRules>
                        </rewrite>";
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(xml));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(context.Request.GetEncodedUrl()));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        // act
        var response = await server.CreateClient().GetStringAsync($"http://localhost/{Guid.NewGuid()}/foo/bar");

        // assert
        Assert.Equal("http://www.test.com/foo/bar", response);
    }

    [Theory]
    [InlineData("http://fetch.environment.local/dev/path", "http://1.1.1.1/path")]
    [InlineData("http://fetch.environment.local/qa/path", "http://fetch.environment.local/qa/path")]
    public async Task Invoke_ReverseProxyToAnotherSiteUsingXmlConfiguredRewriteMap(string requestUri, string expectedRewrittenUri)
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"
                <rewrite>
                    <rules>
                        <rule name=""Proxy"">
                            <match url=""([^/]*)(/?.*)"" />
                            <conditions>
                                <add input=""{environmentMap:{R:1}}"" pattern=""(.+)"" />
                            </conditions>
                            <action type=""Rewrite"" url=""http://{C:1}{R:2}"" appendQueryString=""true"" />
                        </rule>
                    </rules>
                    <rewriteMaps>
                        <rewriteMap name=""environmentMap"">
                            <add key=""dev"" value=""1.1.1.1"" />
                        </rewriteMap>
                    </rewriteMaps>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(context.Request.GetEncodedUrl()));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetStringAsync(new Uri(requestUri));

        Assert.Equal(expectedRewrittenUri, response);
    }

    [Fact]
    public async Task Invoke_CustomResponse()
    {
        var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@"<rewrite>
                <rules>
                <rule name=""Forbidden"">
                <match url = "".*"" />
                <action type=""CustomResponse"" statusCode=""403"" statusReason=""reason"" statusDescription=""description"" />
                </rule>
                </rules>
                </rewrite>"));
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        var response = await server.CreateClient().GetAsync("article/10/hey");
        var content = await response.Content.ReadAsStringAsync();

        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
        Assert.Equal("reason", response.ReasonPhrase);
        Assert.Equal("description", content);
    }

    [Theory]
    [InlineData(@"^http://localhost(/.*)", "http://localhost/foo/bar", (int)UriMatchPart.Path)]
    [InlineData(@"^http://localhost(/.*)", "http://www.test.com/foo/bar", (int)UriMatchPart.Full)]
    public async Task Invoke_GlobalRuleConditionMatchesAgainstFullUri_CodedRule(string conditionInputPattern, string expectedResult, int uriMatchPart)
    {
        // arrange
        var inputParser = new InputParser();

        var ruleBuilder = new UrlRewriteRuleBuilder
        {
            Name = "test",
            Global = false
        };
        ruleBuilder.AddUrlMatch(".*");

        var condition = new UriMatchCondition(
            inputParser,
            "{REQUEST_URI}",
            conditionInputPattern,
            (UriMatchPart)uriMatchPart,
            ignoreCase: true,
            negate: false);
        ruleBuilder.ConfigureConditionBehavior(LogicalGrouping.MatchAll, trackAllCaptures: true);
        ruleBuilder.AddUrlCondition(condition);

        var action = new RewriteAction(
            RuleResult.SkipRemainingRules,
            inputParser.ParseInputString(@"http://www.test.com{C:1}", (UriMatchPart)uriMatchPart),
            queryStringAppend: false);
        ruleBuilder.AddUrlAction(action);

        var options = new RewriteOptions().Add(ruleBuilder.Build());
        using var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                .UseTestServer()
                .Configure(app =>
                {
                    app.UseRewriter(options);
                    app.Run(context => context.Response.WriteAsync(context.Request.GetEncodedUrl()));
                });
            }).Build();

        await host.StartAsync();

        var server = host.GetTestServer();

        // act
        var response = await server.CreateClient().GetStringAsync("http://localhost/foo/bar");

        // assert
        Assert.Equal(expectedResult, response);
    }
}
