using System.Text;
using System.Text.Json;
using NATS.Server.JetStream.Cluster;

namespace NATS.Server.JetStream.Tests.JetStream.Cluster;

/// <summary>
/// Tests for AssignmentCodec: binary serialization for stream and consumer assignments
/// with optional S2/Snappy compression for large payloads.
/// Go reference: jetstream_cluster.go:8703-9246 (encodeAddStreamAssignment,
/// encodeAddConsumerAssignment, decodeStreamAssignment, decodeConsumerAssignment,
/// encodeAddConsumerAssignmentCompressed, decodeConsumerAssignmentCompressed).
/// </summary>
public class AssignmentCodecTests
{
    // ---------------------------------------------------------------
    // StreamAssignment round-trip
    // Go reference: jetstream_cluster.go:8703 encodeAddStreamAssignment /
    //               8733 decodeStreamAssignment
    // ---------------------------------------------------------------

    [Fact]
    public void Encode_decode_stream_assignment_round_trip()
    {
        // Go reference: jetstream_cluster.go:8703 encodeAddStreamAssignment + 8733 decodeStreamAssignment
        var created = new DateTime(2025, 3, 15, 9, 0, 0, DateTimeKind.Utc);
        var sa = new StreamAssignment
        {
            StreamName = "orders",
            Group = new RaftGroup
            {
                Name = "rg-orders",
                Peers = ["peer-1", "peer-2", "peer-3"],
                StorageType = "file",
                Cluster = "cluster-east",
                Preferred = "peer-1",
                DesiredReplicas = 3,
            },
            Created = created,
            ConfigJson = """{"subjects":["orders.>"],"storage":"file","replicas":3}""",
            SyncSubject = "$JS.SYNC.orders",
            Responded = true,
            Recovering = false,
            Reassigning = true,
        };

        var encoded = AssignmentCodec.EncodeStreamAssignment(sa);
        encoded.ShouldNotBeEmpty();

        var decoded = AssignmentCodec.DecodeStreamAssignment(encoded);
        decoded.ShouldNotBeNull();
        decoded!.StreamName.ShouldBe("orders");
        decoded.Group.Name.ShouldBe("rg-orders");
        decoded.Group.Peers.ShouldBe(["peer-1", "peer-2", "peer-3"]);
        decoded.Group.StorageType.ShouldBe("file");
        decoded.Group.Cluster.ShouldBe("cluster-east");
        decoded.Group.Preferred.ShouldBe("peer-1");
        decoded.Group.DesiredReplicas.ShouldBe(3);
        decoded.Created.ShouldBe(created);
        decoded.ConfigJson.ShouldBe("""{"subjects":["orders.>"],"storage":"file","replicas":3}""");
        decoded.SyncSubject.ShouldBe("$JS.SYNC.orders");
        decoded.Responded.ShouldBeTrue();
        decoded.Recovering.ShouldBeFalse();
        decoded.Reassigning.ShouldBeTrue();
    }

    // ---------------------------------------------------------------
    // ConsumerAssignment round-trip
    // Go reference: jetstream_cluster.go:9175 encodeAddConsumerAssignment /
    //               9195 decodeConsumerAssignment
    // ---------------------------------------------------------------

    [Fact]
    public void Encode_decode_consumer_assignment_round_trip()
    {
        // Go reference: jetstream_cluster.go:9175 encodeAddConsumerAssignment + 9195 decodeConsumerAssignment
        var created = new DateTime(2025, 6, 1, 12, 0, 0, DateTimeKind.Utc);
        var ca = new ConsumerAssignment
        {
            ConsumerName = "push-consumer",
            StreamName = "events",
            Group = new RaftGroup
            {
                Name = "rg-push",
                Peers = ["node-a", "node-b"],
                StorageType = "memory",
                DesiredReplicas = 2,
            },
            Created = created,
            ConfigJson = """{"deliver_subject":"push.out","filter_subject":"events.>"}""",
            Responded = true,
            Recovering = true,
        };

        var encoded = AssignmentCodec.EncodeConsumerAssignment(ca);
        encoded.ShouldNotBeEmpty();

        var decoded = AssignmentCodec.DecodeConsumerAssignment(encoded);
        decoded.ShouldNotBeNull();
        decoded!.ConsumerName.ShouldBe("push-consumer");
        decoded.StreamName.ShouldBe("events");
        decoded.Group.Name.ShouldBe("rg-push");
        decoded.Group.Peers.ShouldBe(["node-a", "node-b"]);
        decoded.Group.StorageType.ShouldBe("memory");
        decoded.Group.DesiredReplicas.ShouldBe(2);
        decoded.Created.ShouldBe(created);
        decoded.ConfigJson.ShouldBe("""{"deliver_subject":"push.out","filter_subject":"events.>"}""");
        decoded.Responded.ShouldBeTrue();
        decoded.Recovering.ShouldBeTrue();
    }

    // ---------------------------------------------------------------
    // Error handling
    // Go reference: jetstream_cluster.go:8733 error return on bad unmarshal
    // ---------------------------------------------------------------

    [Fact]
    public void Decode_returns_null_for_invalid_data()
    {
        // Go reference: jetstream_cluster.go:8736 json.Unmarshal error → nil, error
        var garbage = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01, 0x02, 0x03 };
        var result = AssignmentCodec.DecodeStreamAssignment(garbage);
        result.ShouldBeNull();
    }

    [Fact]
    public void Decode_returns_null_for_empty_data()
    {
        // Go reference: jetstream_cluster.go:8733 empty buf → json.Unmarshal fails → nil
        var result = AssignmentCodec.DecodeStreamAssignment(ReadOnlySpan<byte>.Empty);
        result.ShouldBeNull();

        var caResult = AssignmentCodec.DecodeConsumerAssignment(ReadOnlySpan<byte>.Empty);
        caResult.ShouldBeNull();
    }

    // ---------------------------------------------------------------
    // Compression: CompressIfLarge
    // Go reference: jetstream_cluster.go:9226 encodeAddConsumerAssignmentCompressed
    //               uses s2.NewWriter for large consumer configs
    // ---------------------------------------------------------------

    [Fact]
    public void CompressIfLarge_compresses_when_above_threshold()
    {
        // Go reference: jetstream_cluster.go:9226 — S2 compression applied to large consumer assignments
        var largeData = Encoding.UTF8.GetBytes(new string('X', 2048));
        var compressed = AssignmentCodec.CompressIfLarge(largeData, threshold: 1024);

        // Snappy compressed data with the stream magic is larger for uniform input but will differ
        // The important thing is that the result is NOT the same bytes as the input
        compressed.ShouldNotBeSameAs(largeData);
        // Compressed form of repeated bytes should typically be shorter
        compressed.Length.ShouldBeLessThan(largeData.Length);
    }

    [Fact]
    public void CompressIfLarge_no_compress_below_threshold()
    {
        // Go reference: jetstream_cluster.go — small consumer assignments sent uncompressed
        var smallData = Encoding.UTF8.GetBytes("""{"stream_name":"foo"}""");
        var result = AssignmentCodec.CompressIfLarge(smallData, threshold: 1024);

        result.ShouldBe(smallData);
    }

    // ---------------------------------------------------------------
    // Compression: DecompressIfNeeded
    // Go reference: jetstream_cluster.go:9238 decodeConsumerAssignmentCompressed
    // ---------------------------------------------------------------

    [Fact]
    public void DecompressIfNeeded_decompresses_snappy_data()
    {
        // Go reference: jetstream_cluster.go:9238 decodeConsumerAssignmentCompressed
        var original = Encoding.UTF8.GetBytes("""{"stream_name":"test","group":{"name":"rg"}}""");
        var compressed = AssignmentCodec.CompressIfLarge(original, threshold: 0); // force compress

        var decompressed = AssignmentCodec.DecompressIfNeeded(compressed);
        decompressed.ShouldBe(original);
    }

    [Fact]
    public void DecompressIfNeeded_returns_raw_for_non_compressed()
    {
        // Go reference: jetstream_cluster.go:9195 decodeConsumerAssignment (non-compressed path)
        var plainJson = Encoding.UTF8.GetBytes("""{"stream_name":"test"}""");
        var result = AssignmentCodec.DecompressIfNeeded(plainJson);
        result.ShouldBe(plainJson);
    }

    // ---------------------------------------------------------------
    // Consumer preservation in StreamAssignment round-trip
    // Go reference: jetstream_cluster.go streamAssignment.Consumers map serialization
    // ---------------------------------------------------------------

    [Fact]
    public void Stream_assignment_preserves_consumer_assignments()
    {
        // Go reference: jetstream_cluster.go streamAssignment consumers map preserved in encoding
        var sa = new StreamAssignment
        {
            StreamName = "events",
            Group = new RaftGroup { Name = "rg-events", Peers = ["n1", "n2", "n3"] },
            ConfigJson = """{"subjects":["events.>"]}""",
        };

        sa.Consumers["consumer-alpha"] = new ConsumerAssignment
        {
            ConsumerName = "consumer-alpha",
            StreamName = "events",
            Group = new RaftGroup { Name = "rg-alpha", Peers = ["n1"] },
            ConfigJson = """{"deliver_subject":"out.alpha"}""",
            Responded = true,
        };
        sa.Consumers["consumer-beta"] = new ConsumerAssignment
        {
            ConsumerName = "consumer-beta",
            StreamName = "events",
            Group = new RaftGroup { Name = "rg-beta", Peers = ["n2"] },
            Recovering = true,
        };
        sa.Consumers["consumer-gamma"] = new ConsumerAssignment
        {
            ConsumerName = "consumer-gamma",
            StreamName = "events",
            Group = new RaftGroup { Name = "rg-gamma", Peers = ["n3"] },
        };

        var encoded = AssignmentCodec.EncodeStreamAssignment(sa);
        var decoded = AssignmentCodec.DecodeStreamAssignment(encoded);

        decoded.ShouldNotBeNull();
        decoded!.Consumers.Count.ShouldBe(3);
        decoded.Consumers["consumer-alpha"].ConsumerName.ShouldBe("consumer-alpha");
        decoded.Consumers["consumer-alpha"].Responded.ShouldBeTrue();
        decoded.Consumers["consumer-beta"].Recovering.ShouldBeTrue();
        decoded.Consumers["consumer-gamma"].Group.Name.ShouldBe("rg-gamma");
    }

    // ---------------------------------------------------------------
    // RaftGroup peer list preservation
    // Go reference: jetstream_cluster.go raftGroup.Peers serialization
    // ---------------------------------------------------------------

    [Fact]
    public void Stream_assignment_preserves_raft_group_peers()
    {
        // Go reference: jetstream_cluster.go:154 raftGroup.Peers in assignment encoding
        var sa = new StreamAssignment
        {
            StreamName = "telemetry",
            Group = new RaftGroup
            {
                Name = "rg-telemetry",
                Peers = ["peer-alpha", "peer-beta", "peer-gamma"],
                DesiredReplicas = 3,
            },
        };

        var encoded = AssignmentCodec.EncodeStreamAssignment(sa);
        var decoded = AssignmentCodec.DecodeStreamAssignment(encoded);

        decoded.ShouldNotBeNull();
        decoded!.Group.Peers.Count.ShouldBe(3);
        decoded.Group.Peers.ShouldContain("peer-alpha");
        decoded.Group.Peers.ShouldContain("peer-beta");
        decoded.Group.Peers.ShouldContain("peer-gamma");
        decoded.Group.DesiredReplicas.ShouldBe(3);
    }

    // ---------------------------------------------------------------
    // Large ConfigJson round-trip through compression
    // Go reference: jetstream_cluster.go:9226 encodeAddConsumerAssignmentCompressed
    //               for large consumer configs
    // ---------------------------------------------------------------

    [Fact]
    public void Compress_decompress_round_trip_with_large_config()
    {
        // Go reference: jetstream_cluster.go:9226 — compressed consumer assignment with large config
        var largeConfig = """{"subjects":[""" +
            string.Join(",", Enumerable.Range(1, 50).Select(i => $"\"events.topic.{i}.>\"")) +
            """],"storage":"file","replicas":3,"max_msgs":1000000,"max_bytes":1073741824}""";

        var ca = new ConsumerAssignment
        {
            ConsumerName = "large-config-consumer",
            StreamName = "big-stream",
            Group = new RaftGroup
            {
                Name = "rg-large",
                Peers = ["n1", "n2", "n3"],
            },
            ConfigJson = largeConfig,
        };

        var encoded = AssignmentCodec.EncodeConsumerAssignment(ca);
        var compressed = AssignmentCodec.CompressIfLarge(encoded, threshold: 512);

        // Compressed should be present (input is large)
        compressed.Length.ShouldBeGreaterThan(0);

        var decompressed = AssignmentCodec.DecompressIfNeeded(compressed);
        var decoded = AssignmentCodec.DecodeConsumerAssignment(decompressed);

        decoded.ShouldNotBeNull();
        decoded!.ConsumerName.ShouldBe("large-config-consumer");
        decoded.ConfigJson.ShouldBe(largeConfig);
        decoded.Group.Peers.Count.ShouldBe(3);
    }

    // ---------------------------------------------------------------
    // Golden fixture test: known-good JSON bytes decode correctly
    // Go reference: jetstream_cluster.go decodeStreamAssignment / decodeConsumerAssignment
    // ---------------------------------------------------------------

    [Fact]
    public void Golden_fixture_known_bytes()
    {
        // Go reference: jetstream_cluster.go:8733 decodeStreamAssignment — format stability test.
        // This fixture encodes a specific known StreamAssignment JSON and verifies that
        // the codec can decode it correctly, ensuring the serialization format remains stable.
        //
        // The JSON uses snake_case property names (JsonNamingPolicy.SnakeCaseLower).
        // Created timestamp: 2025-01-15T00:00:00Z = 638717280000000000 ticks.
        const string goldenJson = """
            {
                "stream_name": "golden-stream",
                "group": {
                    "name": "rg-golden",
                    "peers": ["node-1", "node-2", "node-3"],
                    "storage_type": "file",
                    "cluster": "us-east",
                    "preferred": "node-1",
                    "desired_replicas": 3
                },
                "created": "2025-01-15T00:00:00Z",
                "config_json": "{\"subjects\":[\"golden.>\"]}",
                "sync_subject": "$JS.SYNC.golden-stream",
                "responded": true,
                "recovering": false,
                "reassigning": false,
                "consumers": {}
            }
            """;

        var bytes = Encoding.UTF8.GetBytes(goldenJson);
        var decoded = AssignmentCodec.DecodeStreamAssignment(bytes);

        decoded.ShouldNotBeNull();
        decoded!.StreamName.ShouldBe("golden-stream");
        decoded.Group.Name.ShouldBe("rg-golden");
        decoded.Group.Peers.ShouldBe(["node-1", "node-2", "node-3"]);
        decoded.Group.StorageType.ShouldBe("file");
        decoded.Group.Cluster.ShouldBe("us-east");
        decoded.Group.Preferred.ShouldBe("node-1");
        decoded.Group.DesiredReplicas.ShouldBe(3);
        decoded.Created.ShouldBe(new DateTime(2025, 1, 15, 0, 0, 0, DateTimeKind.Utc));
        decoded.ConfigJson.ShouldBe("""{"subjects":["golden.>"]}""");
        decoded.SyncSubject.ShouldBe("$JS.SYNC.golden-stream");
        decoded.Responded.ShouldBeTrue();
        decoded.Recovering.ShouldBeFalse();
        decoded.Reassigning.ShouldBeFalse();
        decoded.Consumers.ShouldBeEmpty();
    }
}
