using System.Buffers.Binary;
using System.IO.Compression;
using System.Text;
using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core.Compression;
using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Shared;

namespace ZB.MOM.WW.CBDD.Tests;

public class CompressionCorruptionTests
{
    /// <summary>
    ///     Verifies corrupted compressed payload checksum triggers invalid data errors.
    /// </summary>
    [Fact]
    public void Read_WithBadChecksum_ShouldThrowInvalidData()
    {
        string dbPath = NewDbPath();
        var options = CompressionEnabledOptions();

        try
        {
            using var db = new TestDbContext(dbPath, options);
            var id = InsertCheckpointAndCorrupt(db, header =>
            {
                uint currentChecksum = BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(12, 4));
                BinaryPrimitives.WriteUInt32LittleEndian(header.Slice(12, 4), currentChecksum + 1);
            });

            var ex = Should.Throw<InvalidDataException>(() => db.Users.FindById(id));
            ex.Message.ShouldContain("checksum mismatch");
            db.GetCompressionStats().ChecksumFailureCount.ShouldBeGreaterThanOrEqualTo(1);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies invalid original length metadata triggers invalid data errors.
    /// </summary>
    [Fact]
    public void Read_WithBadOriginalLength_ShouldThrowInvalidData()
    {
        string dbPath = NewDbPath();
        var options = CompressionEnabledOptions();

        try
        {
            using var db = new TestDbContext(dbPath, options);
            var id = InsertCheckpointAndCorrupt(db,
                header => { BinaryPrimitives.WriteInt32LittleEndian(header.Slice(4, 4), -1); });

            var ex = Should.Throw<InvalidDataException>(() => db.Users.FindById(id));
            ex.Message.ShouldContain("decompress");
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies oversized declared decompressed length enforces safety guardrails.
    /// </summary>
    [Fact]
    public void Read_WithOversizedDeclaredLength_ShouldEnforceGuardrail()
    {
        string dbPath = NewDbPath();
        var options = CompressionEnabledOptions(2048);

        try
        {
            using var db = new TestDbContext(dbPath, options);
            var id = InsertCheckpointAndCorrupt(db,
                header => { BinaryPrimitives.WriteInt32LittleEndian(header.Slice(4, 4), 2049); });

            var ex = Should.Throw<InvalidDataException>(() => db.Users.FindById(id));
            ex.Message.ShouldContain("invalid decompressed length");
            db.GetCompressionStats().SafetyLimitRejectionCount.ShouldBeGreaterThanOrEqualTo(1);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies invalid codec identifiers in compressed headers trigger invalid data errors.
    /// </summary>
    [Fact]
    public void Read_WithInvalidCodecId_ShouldThrowInvalidData()
    {
        string dbPath = NewDbPath();
        var options = CompressionEnabledOptions();

        try
        {
            using var db = new TestDbContext(dbPath, options);
            var id = InsertCheckpointAndCorrupt(db, header =>
            {
                header[0] = 0; // CompressionCodec.None is invalid for compressed payload header.
            });

            var ex = Should.Throw<InvalidDataException>(() => db.Users.FindById(id));
            ex.Message.ShouldContain("invalid codec");
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    private static ObjectId InsertCheckpointAndCorrupt(TestDbContext db, HeaderMutator mutateHeader)
    {
        var user = new User
        {
            Name = BuildPayload(16_000),
            Age = 33
        };

        var id = db.Users.Insert(user);
        db.SaveChanges();
        db.ForceCheckpoint();

        (uint pageId, var slot, _) = FindFirstCompressedSlot(db.Storage);
        ((slot.Flags & SlotFlags.HasOverflow) != 0).ShouldBeFalse();

        var page = new byte[db.Storage.PageSize];
        db.Storage.ReadPage(pageId, null, page);
        var headerSlice = page.AsSpan(slot.Offset, CompressedPayloadHeader.Size);
        mutateHeader(headerSlice);
        db.Storage.WritePageImmediate(pageId, page);

        return id;
    }

    private static (uint PageId, SlotEntry Slot, ushort SlotIndex) FindFirstCompressedSlot(StorageEngine storage)
    {
        var buffer = new byte[storage.PageSize];
        for (uint pageId = 1; pageId < storage.PageCount; pageId++)
        {
            storage.ReadPage(pageId, null, buffer);
            var header = SlottedPageHeader.ReadFrom(buffer);
            if (header.PageType != PageType.Data)
                continue;

            for (ushort slotIndex = 0; slotIndex < header.SlotCount; slotIndex++)
            {
                int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size;
                var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset, SlotEntry.Size));
                if ((slot.Flags & SlotFlags.Deleted) != 0)
                    continue;

                if ((slot.Flags & SlotFlags.Compressed) != 0)
                    return (pageId, slot, slotIndex);
            }
        }

        throw new InvalidOperationException("No active compressed slot found for corruption test setup.");
    }

    private static CompressionOptions CompressionEnabledOptions(int maxDecompressedSizeBytes = 32 * 1024)
    {
        return new CompressionOptions
        {
            EnableCompression = true,
            MinSizeBytes = 0,
            MinSavingsPercent = 0,
            Codec = CompressionCodec.Brotli,
            Level = CompressionLevel.Fastest,
            MaxDecompressedSizeBytes = maxDecompressedSizeBytes
        };
    }

    private static string BuildPayload(int approxLength)
    {
        var builder = new StringBuilder(approxLength + 256);
        var i = 0;
        while (builder.Length < approxLength)
        {
            builder.Append("corruption-payload-");
            builder.Append(i.ToString("D8"));
            builder.Append('|');
            i++;
        }

        return builder.ToString();
    }

    private static string NewDbPath()
    {
        return Path.Combine(Path.GetTempPath(), $"compression_corruption_{Guid.NewGuid():N}.db");
    }

    private static void CleanupFiles(string dbPath)
    {
        string walPath = Path.ChangeExtension(dbPath, ".wal");
        var markerPath = $"{dbPath}.compact.state";
        if (File.Exists(dbPath)) File.Delete(dbPath);
        if (File.Exists(walPath)) File.Delete(walPath);
        if (File.Exists(markerPath)) File.Delete(markerPath);
    }

    private delegate void HeaderMutator(Span<byte> header);
}