using System.Text.Json;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.Management;
using ScadaLink.Commons.Types.InboundApi;
using ScadaLink.ManagementService;

namespace ScadaLink.ManagementService.Tests;

/// <summary>
/// ConfigurationDatabase-012: creating an API key must generate a random key,
/// persist only its peppered hash, and return the plaintext to the caller exactly
/// once. The plaintext must never reach the stored entity.
/// </summary>
public class ApiKeyCreationTests : TestKit, IDisposable
{
    private const string Pepper = "a-sufficiently-long-server-side-pepper-value";

    private readonly ServiceCollection _services = new();
    private readonly IInboundApiRepository _apiRepo = Substitute.For<IInboundApiRepository>();
    private readonly IAuditService _auditService = Substitute.For<IAuditService>();

    public ApiKeyCreationTests()
    {
        _services.AddScoped(_ => _apiRepo);
        _services.AddScoped(_ => _auditService);
        _services.AddSingleton<IApiKeyHasher>(new ApiKeyHasher(Pepper));
    }

    private IActorRef CreateActor()
    {
        var sp = _services.BuildServiceProvider();
        return Sys.ActorOf(Props.Create(() => new ManagementActor(sp, NullLogger<ManagementActor>.Instance)));
    }

    private static ManagementEnvelope Envelope(object command, params string[] roles) =>
        new(new AuthenticatedUser("admin", "Admin User", roles, Array.Empty<string>()),
            command, Guid.NewGuid().ToString("N"));

    void IDisposable.Dispose() => Shutdown();

    [Fact]
    public void CreateApiKey_PersistsOnlyHash_NeverPlaintext()
    {
        ApiKey? persisted = null;
        _apiRepo.AddApiKeyAsync(Arg.Do<ApiKey>(k => persisted = k))
            .Returns(Task.CompletedTask);

        var actor = CreateActor();
        actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production"), "Admin"));

        var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));

        // The response must carry the one-time plaintext key shown to the operator.
        var plaintext = ExtractPlaintextKey(response.JsonData);
        Assert.False(string.IsNullOrWhiteSpace(plaintext));

        // The stored entity must carry a hash, never the plaintext.
        Assert.NotNull(persisted);
        Assert.NotEqual(plaintext, persisted!.KeyHash);

        // The persisted hash must equal the peppered hash of the returned plaintext.
        var hasher = new ApiKeyHasher(Pepper);
        Assert.Equal(hasher.Hash(plaintext!), persisted.KeyHash);
    }

    [Fact]
    public void CreateApiKey_ResponseDoesNotEchoTheHash()
    {
        ApiKey? persisted = null;
        _apiRepo.AddApiKeyAsync(Arg.Do<ApiKey>(k => persisted = k))
            .Returns(Task.CompletedTask);

        var actor = CreateActor();
        actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production"), "Admin"));

        var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));

        Assert.NotNull(persisted);
        // The serialized response must not leak the stored hash as a usable artifact.
        Assert.DoesNotContain(persisted!.KeyHash, response.JsonData);
    }

    [Fact]
    public void CreateApiKey_TwoKeys_GenerateDistinctRandomValues()
    {
        var hashes = new List<string>();
        _apiRepo.AddApiKeyAsync(Arg.Do<ApiKey>(k => hashes.Add(k.KeyHash)))
            .Returns(Task.CompletedTask);

        var actor = CreateActor();
        actor.Tell(Envelope(new CreateApiKeyCommand("KeyA"), "Admin"));
        ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
        actor.Tell(Envelope(new CreateApiKeyCommand("KeyB"), "Admin"));
        ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));

        Assert.Equal(2, hashes.Count);
        Assert.NotEqual(hashes[0], hashes[1]);
    }

    /// <summary>
    /// The create response is JSON carrying the one-time plaintext key under a
    /// <c>PlaintextKey</c> (or <c>Key</c>) property.
    /// </summary>
    private static string? ExtractPlaintextKey(string json)
    {
        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        if (root.TryGetProperty("plaintextKey", out var p) || root.TryGetProperty("PlaintextKey", out p))
            return p.GetString();
        if (root.TryGetProperty("key", out var k) || root.TryGetProperty("Key", out k))
            return k.GetString();
        return null;
    }
}
