using System.Collections.Generic;
using ErsatzTV.Filters;
using ErsatzTV.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Filters;

/// <summary>
///     Proves the shared <see cref="ApiAuthorizationFilter.EndpointRequiresKey" /> predicate — the one
///     the OpenAPI security/401 transformer consumes — agrees with what the filter actually enforces at
///     runtime, across a representative matrix. If the two ever diverged, the spec could claim an endpoint
///     is open while the filter gates it (or vice-versa); this test is the anti-drift guard (#287).
/// </summary>
[TestFixture]
public class ApiKeyEndpointRequiresKeyTests
{
    private const string Key = "secret";

    private sealed class FakeApiKeyProvider(bool requireKeyForReads) : IApiKeyProvider
    {
        public string ApiKey => Key;
        public bool RequireKeyForReads { get; } = requireKeyForReads;
    }

    // (method, requiresApiKey, skip, requireKeyForReads, expectedRequiresKey)
    private static readonly object[] Matrix =
    [
        new object[] { "POST", false, false, false, true },   // write always gated
        new object[] { "PUT", false, false, false, true },
        new object[] { "PATCH", false, false, false, true },
        new object[] { "DELETE", false, false, false, true },
        new object[] { "GET", false, false, true, true },     // read gated when reads-required
        new object[] { "GET", false, false, false, false },   // read open when reads-not-required
        new object[] { "GET", true, false, false, true },     // [RequiresAuthentication] gates read even so
        new object[] { "HEAD", false, false, false, false },  // read verb, open
        new object[] { "OPTIONS", false, false, true, false },// preflight always exempt
        new object[] { "POST", false, true, false, false }    // [SkipApiAuthorization] exempt
    ];

    [TestCaseSource(nameof(Matrix))]
    public void Predicate_Matches_Expected(
        string method,
        bool requiresApiKey,
        bool skip,
        bool requireKeyForReads,
        bool expected)
    {
        var metadata = new List<object>();
        if (requiresApiKey)
        {
            metadata.Add(new RequiresAuthenticationAttribute());
        }

        if (skip)
        {
            metadata.Add(new SkipApiAuthorizationAttribute());
        }

        ApiAuthorizationFilter.EndpointRequiresKey(method, metadata, requireKeyForReads)
            .ShouldBe(expected);
    }

    [TestCaseSource(nameof(Matrix))]
    public void Predicate_Agrees_With_Filter_Enforcement(
        string method,
        bool requiresApiKey,
        bool skip,
        bool requireKeyForReads,
        bool expected)
    {
        // The filter, on an /api path with the header MISSING, produces a 401 exactly when the predicate
        // says the endpoint requires a key. Drive the real filter and compare its decision to the predicate.
        AuthorizationFilterContext context = MakeContext(method, requiresApiKey, skip);
        new ApiAuthorizationFilter(new FakeApiKeyProvider(requireKeyForReads)).OnAuthorization(context);

        bool filterGated = context.Result is not null;
        filterGated.ShouldBe(expected);
        filterGated.ShouldBe(
            ApiAuthorizationFilter.EndpointRequiresKey(method, Metadata(requiresApiKey, skip), requireKeyForReads));
    }

    [Test]
    public void Filter_Never_Gates_Non_Api_Paths_Even_When_Predicate_Would()
    {
        // The predicate assumes an /api endpoint; the filter's path scoping precedes it. A mutating request
        // outside /api must pass untouched even though the predicate (given the same method) returns true.
        ApiAuthorizationFilter.EndpointRequiresKey("POST", new List<object>(), requireKeyForReads: true)
            .ShouldBeTrue();

        AuthorizationFilterContext context = MakeContext("POST", requiresApiKey: false, skip: false, path: "/iptv/x.m3u");
        new ApiAuthorizationFilter(new FakeApiKeyProvider(requireKeyForReads: true)).OnAuthorization(context);

        context.Result.ShouldBeNull();
    }

    private static List<object> Metadata(bool requiresApiKey, bool skip)
    {
        var metadata = new List<object>();
        if (requiresApiKey)
        {
            metadata.Add(new RequiresAuthenticationAttribute());
        }

        if (skip)
        {
            metadata.Add(new SkipApiAuthorizationAttribute());
        }

        return metadata;
    }

    private static AuthorizationFilterContext MakeContext(
        string method,
        bool requiresApiKey,
        bool skip,
        string path = "/api/v1/channels")
    {
        var httpContext = new DefaultHttpContext();
        httpContext.Request.Method = method;
        httpContext.Request.Path = path;

        var actionDescriptor = new ActionDescriptor { EndpointMetadata = Metadata(requiresApiKey, skip) };
        var actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor);
        return new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());
    }
}
