using Akka.Actor;
using Akka.Serialization;
using Akka.TestKit.Xunit2;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Protocol;

namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Tests;

/// <summary>
/// Wire-contract tests. Secret ciphertext crosses the network as these bytes, so the round-trip is
/// asserted field-by-field rather than by equality alone — a silently dropped crypto field would
/// produce a row that stores fine and fails to decrypt much later, on another node.
/// </summary>
public sealed class SecretReplicationSerializerTests : TestKit
{
    public SecretReplicationSerializerTests()
        : base(AkkaSecretsReplication.SerializationConfig)
    {
    }

    private static StoredSecret Row(string name) => new()
    {
        Name = new SecretName(name),
        Description = "a description",
        ContentType = SecretContentType.ConnectionString,
        Ciphertext = [1, 2, 3, 250],
        Nonce = [4, 5, 6],
        Tag = [7, 8, 9],
        WrappedDek = [10, 11, 12],
        WrapNonce = [13, 14, 15],
        WrapTag = [16, 17, 18],
        KekId = "sha256:abc",
        Revision = 42,
        IsDeleted = true,
        DeletedUtc = new DateTimeOffset(2026, 3, 2, 1, 0, 0, TimeSpan.Zero),
        CreatedUtc = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero),
        UpdatedUtc = new DateTimeOffset(2026, 2, 1, 0, 0, 0, TimeSpan.Zero),
        CreatedBy = "alice",
        UpdatedBy = "bob",
    };

    private object RoundTrip(object message)
    {
        Serializer serializer = Sys.Serialization.FindSerializerFor(message);
        Assert.IsType<SecretReplicationSerializer>(serializer);

        byte[] bytes = serializer.ToBinary(message);
        string manifest = ((SerializerWithStringManifest)serializer).Manifest(message);

        return ((SerializerWithStringManifest)serializer).FromBinary(bytes, manifest);
    }

    [Fact]
    public void SecretRowsMessage_round_trips_every_field()
    {
        StoredSecret original = Row("app/db-conn");
        var message = new SecretRowsMessage([SecretRowDto.FromStoredSecret(original)]);

        var restored = (SecretRowsMessage)RoundTrip(message);
        StoredSecret result = restored.Rows.Single().ToStoredSecret();

        Assert.Equal(original.Name.Value, result.Name.Value);
        Assert.Equal(original.Description, result.Description);
        Assert.Equal(original.ContentType, result.ContentType);
        Assert.Equal(original.Ciphertext, result.Ciphertext);
        Assert.Equal(original.Nonce, result.Nonce);
        Assert.Equal(original.Tag, result.Tag);
        Assert.Equal(original.WrappedDek, result.WrappedDek);
        Assert.Equal(original.WrapNonce, result.WrapNonce);
        Assert.Equal(original.WrapTag, result.WrapTag);
        Assert.Equal(original.KekId, result.KekId);
        Assert.Equal(original.Revision, result.Revision);
        Assert.Equal(original.IsDeleted, result.IsDeleted);
        Assert.Equal(original.DeletedUtc, result.DeletedUtc);
        Assert.Equal(original.CreatedUtc, result.CreatedUtc);
        Assert.Equal(original.UpdatedUtc, result.UpdatedUtc);
        Assert.Equal(original.CreatedBy, result.CreatedBy);
        Assert.Equal(original.UpdatedBy, result.UpdatedBy);
    }

    [Fact]
    public void Timestamps_survive_the_wire_exactly()
    {
        // Last-writer-wins compares UpdatedUtc for EQUALITY to break revision ties. A serializer that
        // rounded to the millisecond would turn a tie into a spurious "newer" and two nodes would
        // then overwrite each other forever, so tick-exactness is a correctness requirement.
        StoredSecret original = Row("app/precise") with
        {
            UpdatedUtc = new DateTimeOffset(2026, 2, 1, 12, 34, 56, TimeSpan.Zero).AddTicks(1234567),
        };

        var restored = (SecretRowsMessage)RoundTrip(
            new SecretRowsMessage([SecretRowDto.FromStoredSecret(original)]));

        Assert.Equal(original.UpdatedUtc, restored.Rows.Single().ToStoredSecret().UpdatedUtc);
        Assert.Equal(original.UpdatedUtc.Ticks, restored.Rows.Single().ToStoredSecret().UpdatedUtc.Ticks);
    }

    [Fact]
    public void ManifestAnnounce_round_trips()
    {
        var message = new SecretManifestAnnounce(
        [
            SecretManifestEntryDto.FromEntry(new SecretManifestEntry
            {
                Name = new SecretName("a/one"),
                Revision = 3,
                UpdatedUtc = new DateTimeOffset(2026, 5, 5, 5, 5, 5, TimeSpan.Zero),
                IsDeleted = true,
            }),
        ]);

        var restored = (SecretManifestAnnounce)RoundTrip(message);
        SecretManifestEntry entry = restored.Entries.Single().ToEntry();

        Assert.Equal("a/one", entry.Name.Value);
        Assert.Equal(3, entry.Revision);
        Assert.True(entry.IsDeleted);
        Assert.Equal(new DateTimeOffset(2026, 5, 5, 5, 5, 5, TimeSpan.Zero), entry.UpdatedUtc);
    }

    [Fact]
    public void PullRequest_round_trips()
    {
        var restored = (SecretPullRequest)RoundTrip(new SecretPullRequest(["a/one", "b/two"]));

        Assert.Equal(["a/one", "b/two"], restored.Names);
    }

    [Fact]
    public void An_unknown_manifest_fails_loudly_rather_than_guessing()
    {
        // A mixed-version cluster must fail on an unrecognized payload, not mis-deserialize a secret.
        var serializer = (SerializerWithStringManifest)Sys.Serialization
            .FindSerializerFor(new SecretPullRequest([]));

        Assert.Throws<System.Runtime.Serialization.SerializationException>(
            () => serializer.FromBinary([1, 2, 3], "zbs-from-the-future-v9"));
    }

    [Fact]
    public void The_wire_contract_carries_no_field_that_could_hold_key_material()
    {
        // Structural guard on the trust boundary: the DTO's surface is fixed and reviewed. If someone
        // adds a property to SecretRowDto, this test fails and forces a deliberate look at whether
        // the new field is safe to put on the wire.
        string[] properties = [.. typeof(SecretRowDto)
            .GetProperties()
            .Select(p => p.Name)
            .Order(StringComparer.Ordinal)];

        Assert.Equal(
            [
                "Ciphertext", "ContentType", "CreatedBy", "CreatedUtc", "DeletedUtc", "Description",
                "IsDeleted", "KekId", "Name", "Nonce", "Revision", "Tag", "UpdatedBy", "UpdatedUtc",
                "WrapNonce", "WrapTag", "WrappedDek",
            ],
            properties);
    }
}
