using NSubstitute;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Interfaces.Repositories;

namespace ScadaLink.InboundAPI.Tests;

/// <summary>
/// WP-1: Tests for API key validation — X-API-Key header, enabled/disabled keys,
/// method approval.
/// </summary>
public class ApiKeyValidatorTests
{
    private readonly IInboundApiRepository _repository = Substitute.For<IInboundApiRepository>();
    private readonly ApiKeyValidator _validator;

    public ApiKeyValidatorTests()
    {
        _validator = new ApiKeyValidator(_repository);
    }

    [Fact]
    public async Task MissingApiKey_Returns401()
    {
        var result = await _validator.ValidateAsync(null, "testMethod");
        Assert.False(result.IsValid);
        Assert.Equal(401, result.StatusCode);
    }

    [Fact]
    public async Task EmptyApiKey_Returns401()
    {
        var result = await _validator.ValidateAsync("", "testMethod");
        Assert.False(result.IsValid);
        Assert.Equal(401, result.StatusCode);
    }

    [Fact]
    public async Task InvalidApiKey_Returns401()
    {
        _repository.GetAllApiKeysAsync().Returns(new List<ApiKey>());

        var result = await _validator.ValidateAsync("bad-key", "testMethod");
        Assert.False(result.IsValid);
        Assert.Equal(401, result.StatusCode);
    }

    [Fact]
    public async Task DisabledApiKey_Returns401()
    {
        var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = false };
        _repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });

        var result = await _validator.ValidateAsync("valid-key", "testMethod");
        Assert.False(result.IsValid);
        Assert.Equal(401, result.StatusCode);
    }

    [Fact]
    public async Task ValidKey_MethodNotFound_Returns400()
    {
        var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
        _repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
        _repository.GetMethodByNameAsync("nonExistent").Returns((ApiMethod?)null);

        var result = await _validator.ValidateAsync("valid-key", "nonExistent");
        Assert.False(result.IsValid);
        Assert.Equal(400, result.StatusCode);
    }

    [Fact]
    public async Task ValidKey_NotApprovedForMethod_Returns403()
    {
        var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
        var method = new ApiMethod("testMethod", "return 1;") { Id = 10 };

        _repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
        _repository.GetMethodByNameAsync("testMethod").Returns(method);
        _repository.GetApprovedKeysForMethodAsync(10).Returns(new List<ApiKey>());

        var result = await _validator.ValidateAsync("valid-key", "testMethod");
        Assert.False(result.IsValid);
        Assert.Equal(403, result.StatusCode);
    }

    [Fact]
    public async Task ValidKey_ApprovedForMethod_ReturnsValid()
    {
        var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
        var method = new ApiMethod("testMethod", "return 1;") { Id = 10 };

        _repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
        _repository.GetMethodByNameAsync("testMethod").Returns(method);
        _repository.GetApprovedKeysForMethodAsync(10).Returns(new List<ApiKey> { key });

        var result = await _validator.ValidateAsync("valid-key", "testMethod");
        Assert.True(result.IsValid);
        Assert.Equal(200, result.StatusCode);
        Assert.Equal(key, result.ApiKey);
        Assert.Equal(method, result.Method);
    }

    // --- InboundAPI-003: API key must not be matched with a non-constant-time
    // (timing-oracle) secret-equality lookup. ---

    [Fact]
    public async Task ValidateAsync_DoesNotUseSecretEqualityLookup()
    {
        // GetApiKeyByValueAsync translates to a SQL "WHERE KeyValue = @secret" early-exit
        // comparison — a timing side-channel. The validator must not call it.
        var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
        var method = new ApiMethod("testMethod", "return 1;") { Id = 10 };

        _repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
        _repository.GetMethodByNameAsync("testMethod").Returns(method);
        _repository.GetApprovedKeysForMethodAsync(10).Returns(new List<ApiKey> { key });

        await _validator.ValidateAsync("valid-key", "testMethod");

        await _repository.DidNotReceive()
            .GetApiKeyByValueAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateAsync_WrongKey_ConstantTimePath_Returns401()
    {
        var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
        _repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });

        var result = await _validator.ValidateAsync("wrong-key", "testMethod");

        Assert.False(result.IsValid);
        Assert.Equal(401, result.StatusCode);
    }

    [Fact]
    public async Task ValidateAsync_KeyOfDifferentLength_Returns401()
    {
        // FixedTimeEquals over UTF-8 bytes must reject length mismatches without leaking.
        var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
        _repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });

        var result = await _validator.ValidateAsync("valid-key-with-extra", "testMethod");

        Assert.False(result.IsValid);
        Assert.Equal(401, result.StatusCode);
    }
}
