using System.Security.Claims;
using System.Net;
using JobTrackerApi.Controllers;
using JobTrackerApi.Services;
using JobTrackerApi.Tests.TestSupport;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class ExternalOriginTests
{
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("http://jobs.example.test")]
    [InlineData("https://user@jobs.example.test")]
    [InlineData("https://jobs.example.test/path")]
    [InlineData("https://jobs.example.test?query=1")]
    [InlineData("https://jobs.example.test#fragment")]
    public void Production_requires_one_clean_https_origin(string? value)
    {
        Assert.Throws<InvalidOperationException>(() => ExternalOrigin.Parse(value, production: true));
    }

    [Fact]
    public void Development_defaults_to_local_frontend()
    {
        Assert.Equal("http://localhost:3000", ExternalOrigin.Parse(null, production: false).BaseUrl);
    }

    [Fact]
    public void Canonical_host_matching_includes_the_configured_port()
    {
        var origin = ExternalOrigin.Parse("https://jobs.example.test:8443/", production: true);

        Assert.True(origin.Matches(new HostString("jobs.example.test", 8443)));
        Assert.False(origin.Matches(new HostString("jobs.example.test")));
        Assert.False(origin.Matches(new HostString("attacker.example.test", 8443)));
    }

    [Fact]
    public void Internal_host_is_allowed_only_for_liveness()
    {
        var origin = ExternalOrigin.Parse("https://jobs.example.test", production: true);

        Assert.True(origin.AllowsRequest(new HostString("jobs.example.test"), "/api/auth/config"));
        Assert.True(origin.AllowsRequest(new HostString("localhost", 8080), "/health"));
        Assert.False(origin.AllowsRequest(new HostString("localhost", 8080), "/api/auth/config"));
        Assert.False(origin.AllowsRequest(new HostString("attacker.example.test"), "/health"));
    }

    [Fact]
    public void Forwarded_headers_require_an_explicit_proxy_network()
    {
        var missing = BuildConfig(new Dictionary<string, string?>());
        var invalid = BuildConfig(new Dictionary<string, string?> { ["Proxy:KnownNetworks:0"] = "anywhere" });

        Assert.Throws<InvalidOperationException>(() => ForwardedProxyConfiguration.Build(missing));
        Assert.Throws<InvalidOperationException>(() => ForwardedProxyConfiguration.Build(invalid));
    }

    [Fact]
    public void Forwarded_headers_trust_one_hop_from_the_configured_network_only()
    {
        var config = BuildConfig(new Dictionary<string, string?> { ["Proxy:KnownNetworks:0"] = "172.31.250.0/29" });

        var options = ForwardedProxyConfiguration.Build(config);

        Assert.Equal(1, options.ForwardLimit);
        var network = Assert.Single(options.KnownNetworks);
        Assert.True(network.Contains(IPAddress.Parse("172.31.250.2")));
        Assert.False(network.Contains(IPAddress.Parse("172.31.251.2")));
        Assert.Empty(options.KnownProxies);
    }

    [Fact]
    public void OAuth_callback_ignores_request_host_and_legacy_redirect_override()
    {
        var config = BuildConfig(new Dictionary<string, string?>
        {
            ["App:PublicBaseUrl"] = "https://jobs.example.test",
            ["Microsoft:RedirectUri"] = "https://attacker.example.test/callback",
        });
        var graph = new Mock<IMicrosoftGraphOAuthService>();
        graph.Setup(x => x.BuildAuthorizationUrl("user-1", "https://jobs.example.test/api/microsoft-graph/oauth/callback"))
            .Returns("https://login.microsoftonline.com/authorize");
        var controller = new MicrosoftGraphController(graph.Object, config)
        {
            ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext
                {
                    User = new ClaimsPrincipal(new ClaimsIdentity(new[]
                    {
                        new Claim(ClaimTypes.NameIdentifier, "user-1"),
                    }, "test")),
                },
            },
        };
        controller.Request.Host = new HostString("attacker.example.test");

        controller.ConnectUrl();

        graph.VerifyAll();
    }

    [Fact]
    public void Csrf_cookie_security_comes_from_canonical_origin()
    {
        var config = BuildConfig(new Dictionary<string, string?> { ["App:PublicBaseUrl"] = "https://jobs.example.test" });
        var controller = new AuthController(
            config,
            TestHostFactory.CreateUserManager().Object,
            Mock.Of<ITokenService>(),
            Mock.Of<IAppEmailSender>(),
            Mock.Of<IGoogleTokenValidator>(),
            Mock.Of<IMicrosoftTokenValidator>(),
            NullLogger<AuthController>.Instance,
            Mock.Of<ITwoFactorPendingTokenService>(),
            TestHostFactory.CreateInMemoryDb())
        {
            ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() },
        };
        controller.Request.Scheme = "http";
        controller.Request.Headers["X-Forwarded-Proto"] = "http";

        controller.EnsureCsrfCookie();

        Assert.Contains("secure", controller.Response.Headers.SetCookie.ToString(), StringComparison.OrdinalIgnoreCase);
    }

    private static IConfiguration BuildConfig(IDictionary<string, string?> values) =>
        new ConfigurationBuilder().AddInMemoryCollection(values).Build();
}
