using System.Text;
using System.Text.Json;
using Shouldly;
using ZB.MOM.NatsNet.Server;

namespace ZB.MOM.NatsNet.Server.Tests.JetStream;

public class StoreTypesTests
{
    [Fact]
    public void IsEncodedStreamState_ValidHeader_ReturnsTrue()
    {
        var buffer = new byte[] { 42, 1, 0 };
        StoreParity.IsEncodedStreamState(buffer).ShouldBeTrue();
    }

    [Fact]
    public void IsEncodedStreamState_InvalidHeader_ReturnsFalse()
    {
        StoreParity.IsEncodedStreamState(Array.Empty<byte>()).ShouldBeFalse();
        StoreParity.IsEncodedStreamState(new byte[] { 42 }).ShouldBeFalse();
        StoreParity.IsEncodedStreamState(new byte[] { 41, 1 }).ShouldBeFalse();
        StoreParity.IsEncodedStreamState(new byte[] { 42, 2 }).ShouldBeFalse();
    }

    [Fact]
    public void DecodeStreamState_ValidRunLengthBlock_ReturnsParsedState()
    {
        var buffer = new List<byte> { 42, 1 };
        AppendUVarInt(buffer, 10); // msgs
        AppendUVarInt(buffer, 2048); // bytes
        AppendUVarInt(buffer, 3); // first
        AppendUVarInt(buffer, 12); // last
        AppendUVarInt(buffer, 4); // failed
        AppendUVarInt(buffer, 1); // numDeleted
        buffer.Add(33); // runLengthMagic
        AppendUVarInt(buffer, 5); // first
        AppendUVarInt(buffer, 4); // num

        var (state, error) = StoreParity.DecodeStreamState(buffer.ToArray());

        error.ShouldBeNull();
        state.ShouldNotBeNull();
        state.Msgs.ShouldBe(10UL);
        state.Bytes.ShouldBe(2048UL);
        state.FirstSeq.ShouldBe(3UL);
        state.LastSeq.ShouldBe(12UL);
        state.Failed.ShouldBe(4UL);
        state.Deleted.Count.ShouldBe(1);
        var (first, last, num) = state.Deleted[0].GetState();
        first.ShouldBe(5UL);
        last.ShouldBe(8UL);
        num.ShouldBe(4UL);
    }

    [Fact]
    public void DecodeStreamState_BadHeader_ReturnsBadEncodingError()
    {
        var (state, error) = StoreParity.DecodeStreamState(new byte[] { 1, 1, 1 });
        state.ShouldBeNull();
        error.ShouldBe(StoreErrors.ErrBadStreamStateEncoding);
    }

    [Fact]
    public void DecodeStreamState_CorruptPayload_ReturnsCorruptError()
    {
        var badVarint = new byte[] { 42, 1, 0x80 };
        var (state, error) = StoreParity.DecodeStreamState(badVarint);
        state.ShouldBeNull();
        error.ShouldBe(StoreErrors.ErrCorruptStreamState);
    }

    [Fact]
    public void DecodeStreamState_UnknownDeleteBlockMarker_ReturnsCorruptError()
    {
        var buffer = new List<byte> { 42, 1 };
        AppendUVarInt(buffer, 1);
        AppendUVarInt(buffer, 1);
        AppendUVarInt(buffer, 1);
        AppendUVarInt(buffer, 1);
        AppendUVarInt(buffer, 0);
        AppendUVarInt(buffer, 1);
        buffer.Add(99);

        var (state, error) = StoreParity.DecodeStreamState(buffer.ToArray());
        state.ShouldBeNull();
        error.ShouldBe(StoreErrors.ErrCorruptStreamState);
    }

    [Fact]
    public void DeleteRange_GetState_UsesRunLengthParity()
    {
        var dr = new DeleteRange { First = 7, Num = 3 };
        var (first, last, num) = dr.GetState();
        first.ShouldBe(7UL);
        last.ShouldBe(9UL);
        num.ShouldBe(3UL);
    }

    [Fact]
    public void DeleteSlice_GetState_EmptyAndNonEmptyParity()
    {
        var empty = new DeleteSlice(Array.Empty<ulong>());
        empty.GetState().ShouldBe((0UL, 0UL, 0UL));

        var nonEmpty = new DeleteSlice(new ulong[] { 2, 4, 9 });
        nonEmpty.GetState().ShouldBe((2UL, 9UL, 3UL));
    }

    [Fact]
    public void EncodeConsumerState_WithPendingAndRedelivered_EncodesExpectedShape()
    {
        var pendingTs = 1_700_000_012_000_000_000L; // ns
        var state = new ConsumerState
        {
            AckFloor = new SequencePair { Consumer = 20, Stream = 100 },
            Delivered = new SequencePair { Consumer = 25, Stream = 110 },
            Pending = new Dictionary<ulong, Pending>
            {
                [104] = new Pending { Sequence = 23, Timestamp = pendingTs },
            },
            Redelivered = new Dictionary<ulong, ulong>
            {
                [108] = 2,
            },
        };

        var encoded = StoreParity.EncodeConsumerState(state);

        encoded[0].ShouldBe((byte)22);
        encoded[1].ShouldBe((byte)2);

        var index = 2;
        ReadUVarInt(encoded, ref index).ShouldBe(20UL);
        ReadUVarInt(encoded, ref index).ShouldBe(100UL);
        ReadUVarInt(encoded, ref index).ShouldBe(25UL);
        ReadUVarInt(encoded, ref index).ShouldBe(110UL);
        ReadUVarInt(encoded, ref index).ShouldBe(1UL);

        var minTs = ReadVarInt(encoded, ref index);
        var pendingStreamDelta = ReadUVarInt(encoded, ref index);
        var pendingConsumerDelta = ReadUVarInt(encoded, ref index);
        var pendingTsDelta = ReadVarInt(encoded, ref index);

        (100UL + pendingStreamDelta).ShouldBe(104UL);
        (20UL + pendingConsumerDelta).ShouldBe(23UL);
        (minTs - pendingTsDelta).ShouldBe(pendingTs / 1_000_000_000L);

        ReadUVarInt(encoded, ref index).ShouldBe(1UL);
        var redeliveredStreamDelta = ReadUVarInt(encoded, ref index);
        var redeliveredCount = ReadUVarInt(encoded, ref index);
        (100UL + redeliveredStreamDelta).ShouldBe(108UL);
        redeliveredCount.ShouldBe(2UL);
    }

    [Fact]
    public void IsOutOfSpaceErr_MessageContainsNoSpaceLeft_ReturnsTrue()
    {
        StoreParity.IsOutOfSpaceErr(new IOException("disk full: no space left on device")).ShouldBeTrue();
        StoreParity.IsOutOfSpaceErr(new IOException("permission denied")).ShouldBeFalse();
        StoreParity.IsOutOfSpaceErr(null).ShouldBeFalse();
    }

    [Fact]
    public void IsClusterResetErr_KnownSentinels_ReturnsExpected()
    {
        StoreParity.IsClusterResetErr(StoreParity.ErrLastSeqMismatch).ShouldBeTrue();
        StoreParity.IsClusterResetErr(StoreErrors.ErrStoreEOF).ShouldBeTrue();
        StoreParity.IsClusterResetErr(StoreParity.ErrFirstSequenceMismatch).ShouldBeTrue();
        StoreParity.IsClusterResetErr(new InvalidOperationException("other")).ShouldBeFalse();
    }

    [Fact]
    public void Copy_SourceToDestination_PreservesAndIsolatesBuffers()
    {
        var source = new StoreMsg
        {
            Subject = "ORDERS.created",
            Hdr = Encoding.ASCII.GetBytes("HDR"),
            Msg = Encoding.ASCII.GetBytes("BODY"),
            Buf = Encoding.ASCII.GetBytes("HDRBODY"),
            Seq = 42,
            Ts = 9001,
        };
        var destination = new StoreMsg();

        source.Copy(destination);

        destination.Subject.ShouldBe(source.Subject);
        destination.Seq.ShouldBe(42UL);
        destination.Ts.ShouldBe(9001L);
        destination.Hdr.SequenceEqual(Encoding.ASCII.GetBytes("HDR")).ShouldBeTrue();
        destination.Msg.SequenceEqual(Encoding.ASCII.GetBytes("BODY")).ShouldBeTrue();
        destination.Buf.SequenceEqual(Encoding.ASCII.GetBytes("HDRBODY")).ShouldBeTrue();

        source.Buf[0] = (byte)'X';
        destination.Buf[0].ShouldBe((byte)'H');
    }

    [Fact]
    public void BytesToString_StringToBytes_CopyString_ParityBehavior()
    {
        StoreParity.BytesToString(Array.Empty<byte>()).ShouldBe(string.Empty);
        StoreParity.StringToBytes(string.Empty).ShouldBeNull();

        var raw = new byte[] { 0, 255, 65 };
        var text = StoreParity.BytesToString(raw);
        var roundtrip = StoreParity.StringToBytes(text);

        roundtrip.ShouldNotBeNull();
        roundtrip!.SequenceEqual(raw).ShouldBeTrue();

        var original = new string(new[] { 'n', 'a', 't', 's' });
        var copy = StoreParity.CopyString(original);
        copy.ShouldBe(original);
        ReferenceEquals(copy, original).ShouldBeFalse();
    }

    [Fact]
    public void IsPermissionError_UnauthorizedAccess_ReturnsTrue()
    {
        StoreParity.IsPermissionError(new UnauthorizedAccessException("no access")).ShouldBeTrue();
        StoreParity.IsPermissionError(new IOException("other")).ShouldBeFalse();
        StoreParity.IsPermissionError(null).ShouldBeFalse();
    }

    [Fact]
    public void RetentionPolicy_StringParity_ReturnsExpectedText()
    {
        RetentionPolicy.LimitsPolicy.String().ShouldBe("Limits");
        RetentionPolicy.InterestPolicy.String().ShouldBe("Interest");
        RetentionPolicy.WorkQueuePolicy.String().ShouldBe("WorkQueue");
        ((RetentionPolicy)99).String().ShouldBe("Unknown Retention Policy");
    }

    [Fact]
    public void RetentionPolicy_JsonParity_RoundTripsExpectedTokens()
    {
        JsonSerializer.Serialize(RetentionPolicy.LimitsPolicy).ShouldBe("\"limits\"");
        JsonSerializer.Serialize(RetentionPolicy.InterestPolicy).ShouldBe("\"interest\"");
        JsonSerializer.Serialize(RetentionPolicy.WorkQueuePolicy).ShouldBe("\"workqueue\"");

        JsonSerializer.Deserialize<RetentionPolicy>("\"limits\"").ShouldBe(RetentionPolicy.LimitsPolicy);
        JsonSerializer.Deserialize<RetentionPolicy>("\"interest\"").ShouldBe(RetentionPolicy.InterestPolicy);
        JsonSerializer.Deserialize<RetentionPolicy>("\"workqueue\"").ShouldBe(RetentionPolicy.WorkQueuePolicy);
    }

    [Fact]
    public void RetentionPolicy_UnmarshalInvalid_Throws()
    {
        Should.Throw<JsonException>(() => JsonSerializer.Deserialize<RetentionPolicy>("\"bogus\""));
    }

    [Fact]
    public void DiscardPolicy_StringAndJsonParity_MatchesGo()
    {
        DiscardPolicy.DiscardOld.String().ShouldBe("DiscardOld");
        DiscardPolicy.DiscardNew.String().ShouldBe("DiscardNew");
        ((DiscardPolicy)99).String().ShouldBe("Unknown Discard Policy");

        JsonSerializer.Serialize(DiscardPolicy.DiscardOld).ShouldBe("\"old\"");
        JsonSerializer.Serialize(DiscardPolicy.DiscardNew).ShouldBe("\"new\"");
        JsonSerializer.Deserialize<DiscardPolicy>("\"OLD\"").ShouldBe(DiscardPolicy.DiscardOld);
        JsonSerializer.Deserialize<DiscardPolicy>("\"new\"").ShouldBe(DiscardPolicy.DiscardNew);
    }

    [Fact]
    public void StorageType_StringAndJsonParity_MatchesGo()
    {
        StorageType.MemoryStorage.String().ShouldBe("Memory");
        StorageType.FileStorage.String().ShouldBe("File");
        ((StorageType)99).String().ShouldBe("Unknown Storage Type");

        JsonSerializer.Serialize(StorageType.MemoryStorage).ShouldBe("\"memory\"");
        JsonSerializer.Serialize(StorageType.FileStorage).ShouldBe("\"file\"");
        JsonSerializer.Deserialize<StorageType>("\"memory\"").ShouldBe(StorageType.MemoryStorage);
        JsonSerializer.Deserialize<StorageType>("\"file\"").ShouldBe(StorageType.FileStorage);
    }

    [Fact]
    public void PersistModeType_StringAndJsonParity_MatchesGo()
    {
        PersistModeType.DefaultPersistMode.String().ShouldBe("Default");
        PersistModeType.AsyncPersistMode.String().ShouldBe("Async");
        ((PersistModeType)99).String().ShouldBe("Unknown Persist Mode Type");

        JsonSerializer.Serialize(PersistModeType.DefaultPersistMode).ShouldBe("\"default\"");
        JsonSerializer.Serialize(PersistModeType.AsyncPersistMode).ShouldBe("\"async\"");
        JsonSerializer.Deserialize<PersistModeType>("\"default\"").ShouldBe(PersistModeType.DefaultPersistMode);
        JsonSerializer.Deserialize<PersistModeType>("\"async\"").ShouldBe(PersistModeType.AsyncPersistMode);
    }

    [Fact]
    public void ExternalStream_Domain_ReturnsSecondTokenOrEmpty()
    {
        new ExternalStream().Domain().ShouldBe(string.Empty);
        new ExternalStream { ApiPrefix = "$JS.D1.API" }.Domain().ShouldBe("D1");
    }

    [Fact]
    public void StreamSource_ComposeIName_UsesFilterAndTransforms()
    {
        var source = new StreamSource
        {
            Name = "ORDERS",
            External = new ExternalStream { ApiPrefix = "$JS.EU.API" },
            SubjectTransforms =
            [
                new SubjectTransformConfig { Source = "foo.*", Destination = "bar.*" },
                new SubjectTransformConfig { Source = string.Empty, Destination = "baz.>" },
            ],
        };

        source.ComposeIName().ShouldContain("ORDERS:");
        source.ComposeIName().ShouldContain("foo.*\f>");
        source.ComposeIName().ShouldContain("bar.*\fbaz.>");
        source.SetIndexName();
        source.IndexName.ShouldBe(source.ComposeIName());
    }

    [Fact]
    public void AckPolicy_JsonParity_MatchesGo()
    {
        JsonSerializer.Serialize(AckPolicy.AckNone).ShouldBe("\"none\"");
        JsonSerializer.Serialize(AckPolicy.AckAll).ShouldBe("\"all\"");
        JsonSerializer.Serialize(AckPolicy.AckExplicit).ShouldBe("\"explicit\"");

        JsonSerializer.Deserialize<AckPolicy>("\"none\"").ShouldBe(AckPolicy.AckNone);
        JsonSerializer.Deserialize<AckPolicy>("\"all\"").ShouldBe(AckPolicy.AckAll);
        JsonSerializer.Deserialize<AckPolicy>("\"explicit\"").ShouldBe(AckPolicy.AckExplicit);
        Should.Throw<JsonException>(() => JsonSerializer.Deserialize<AckPolicy>("\"bad\""));
    }

    [Fact]
    public void ReplayPolicy_JsonParity_MatchesGo()
    {
        JsonSerializer.Serialize(ReplayPolicy.ReplayInstant).ShouldBe("\"instant\"");
        JsonSerializer.Serialize(ReplayPolicy.ReplayOriginal).ShouldBe("\"original\"");

        JsonSerializer.Deserialize<ReplayPolicy>("\"instant\"").ShouldBe(ReplayPolicy.ReplayInstant);
        JsonSerializer.Deserialize<ReplayPolicy>("\"original\"").ShouldBe(ReplayPolicy.ReplayOriginal);
        Should.Throw<JsonException>(() => JsonSerializer.Deserialize<ReplayPolicy>("\"bad\""));
    }

    [Fact]
    public void DeliverPolicy_JsonParity_MapsUndefinedToAll()
    {
        JsonSerializer.Serialize(DeliverPolicy.DeliverAll).ShouldBe("\"all\"");
        JsonSerializer.Serialize(DeliverPolicy.DeliverLast).ShouldBe("\"last\"");
        JsonSerializer.Serialize(DeliverPolicy.DeliverLastPerSubject).ShouldBe("\"last_per_subject\"");
        JsonSerializer.Serialize(DeliverPolicy.DeliverNew).ShouldBe("\"new\"");
        JsonSerializer.Serialize(DeliverPolicy.DeliverByStartSequence).ShouldBe("\"by_start_sequence\"");
        JsonSerializer.Serialize(DeliverPolicy.DeliverByStartTime).ShouldBe("\"by_start_time\"");
        JsonSerializer.Serialize((DeliverPolicy)99).ShouldBe("\"undefined\"");

        JsonSerializer.Deserialize<DeliverPolicy>("\"all\"").ShouldBe(DeliverPolicy.DeliverAll);
        JsonSerializer.Deserialize<DeliverPolicy>("\"undefined\"").ShouldBe(DeliverPolicy.DeliverAll);
        JsonSerializer.Deserialize<DeliverPolicy>("\"last\"").ShouldBe(DeliverPolicy.DeliverLast);
        JsonSerializer.Deserialize<DeliverPolicy>("\"last_per_subject\"").ShouldBe(DeliverPolicy.DeliverLastPerSubject);
        JsonSerializer.Deserialize<DeliverPolicy>("\"new\"").ShouldBe(DeliverPolicy.DeliverNew);
        JsonSerializer.Deserialize<DeliverPolicy>("\"by_start_sequence\"").ShouldBe(DeliverPolicy.DeliverByStartSequence);
        JsonSerializer.Deserialize<DeliverPolicy>("\"by_start_time\"").ShouldBe(DeliverPolicy.DeliverByStartTime);
        Should.Throw<JsonException>(() => JsonSerializer.Deserialize<DeliverPolicy>("\"bad\""));
    }

    private static void AppendUVarInt(List<byte> buffer, ulong value)
    {
        while (value >= 0x80)
        {
            buffer.Add((byte)(value | 0x80));
            value >>= 7;
        }
        buffer.Add((byte)value);
    }

    private static ulong ReadUVarInt(byte[] buffer, ref int index)
    {
        ulong value = 0;
        var shift = 0;
        while (index < buffer.Length)
        {
            var b = buffer[index++];
            if (b < 0x80)
            {
                value |= (ulong)b << shift;
                return value;
            }

            value |= (ulong)(b & 0x7F) << shift;
            shift += 7;
        }

        throw new InvalidDataException("Unexpected end of varint");
    }

    private static long ReadVarInt(byte[] buffer, ref int index)
    {
        var uv = ReadUVarInt(buffer, ref index);
        return (long)((uv >> 1) ^ (ulong)-(long)(uv & 1));
    }
}
