using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Core.Transactions;

namespace ZB.MOM.WW.CBDD.Tests;

public class StorageEngineTransactionProtocolTests
{
    /// <summary>
    ///     Verifies preparing an unknown transaction returns false.
    /// </summary>
    [Fact]
    public void PrepareTransaction_Should_ReturnFalse_For_Unknown_Transaction()
    {
        string dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
            storage.PrepareTransaction(999_999).ShouldBeFalse();
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies committing a detached transaction object throws.
    /// </summary>
    [Fact]
    public void CommitTransaction_With_TransactionObject_Should_Throw_When_Not_Active()
    {
        string dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
            var detached = new Transaction(123, storage);

            Should.Throw<InvalidOperationException>(() => storage.CommitTransaction(detached));
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies committing a transaction object persists writes and clears active state.
    /// </summary>
    [Fact]
    public void CommitTransaction_With_TransactionObject_Should_Commit_Writes()
    {
        string dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
            using var txn = storage.BeginTransaction();

            uint pageId = storage.AllocatePage();
            var data = new byte[storage.PageSize];
            data[0] = 0xAB;

            storage.WritePage(pageId, txn.TransactionId, data);
            storage.CommitTransaction(txn);

            storage.ActiveTransactionCount.ShouldBe(0);

            var readBuffer = new byte[storage.PageSize];
            storage.ReadPage(pageId, 0, readBuffer);
            readBuffer[0].ShouldBe((byte)0xAB);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies committing by identifier with no writes does not throw.
    /// </summary>
    [Fact]
    public void CommitTransaction_ById_With_NoWrites_Should_Not_Throw()
    {
        string dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
            storage.CommitTransaction(424242);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies committed transaction cache moves into readable state and active count is cleared.
    /// </summary>
    [Fact]
    public void MarkTransactionCommitted_Should_Move_Cache_And_Clear_ActiveCount()
    {
        string dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
            using var txn = storage.BeginTransaction();

            uint pageId = storage.AllocatePage();
            var data = new byte[storage.PageSize];
            data[5] = 0x5A;
            storage.WritePage(pageId, txn.TransactionId, data);

            storage.ActiveTransactionCount.ShouldBe(1);
            storage.MarkTransactionCommitted(txn.TransactionId);
            storage.ActiveTransactionCount.ShouldBe(0);

            var readBuffer = new byte[storage.PageSize];
            storage.ReadPage(pageId, 0, readBuffer);
            readBuffer[5].ShouldBe((byte)0x5A);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies rollback discards uncommitted page writes.
    /// </summary>
    [Fact]
    public void RollbackTransaction_Should_Discard_Uncommitted_Write()
    {
        string dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);

            uint pageId = storage.AllocatePage();
            var baseline = new byte[storage.PageSize];
            baseline[0] = 0x11;
            storage.WritePageImmediate(pageId, baseline);

            using var txn = storage.BeginTransaction();
            var changed = new byte[storage.PageSize];
            changed[0] = 0x99;
            storage.WritePage(pageId, txn.TransactionId, changed);

            storage.ActiveTransactionCount.ShouldBe(1);
            storage.RollbackTransaction(txn.TransactionId);
            storage.ActiveTransactionCount.ShouldBe(0);

            var readBuffer = new byte[storage.PageSize];
            storage.ReadPage(pageId, 0, readBuffer);
            readBuffer[0].ShouldBe((byte)0x11);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies marking a transaction committed transitions state correctly.
    /// </summary>
    [Fact]
    public void Transaction_MarkCommitted_Should_Transition_State()
    {
        string dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
            using var txn = storage.BeginTransaction();

            uint pageId = storage.AllocatePage();
            var data = new byte[storage.PageSize];
            data[3] = 0x33;
            storage.WritePage(pageId, txn.TransactionId, data);

            txn.MarkCommitted();

            txn.State.ShouldBe(TransactionState.Committed);
            storage.ActiveTransactionCount.ShouldBe(0);

            var readBuffer = new byte[storage.PageSize];
            storage.ReadPage(pageId, 0, readBuffer);
            readBuffer[3].ShouldBe((byte)0x33);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    ///     Verifies preparing then committing writes WAL data and updates transaction state.
    /// </summary>
    [Fact]
    public void Transaction_Prepare_Should_Write_Wal_And_Transition_State()
    {
        string dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
            using var txn = storage.BeginTransaction();

            uint pageId = storage.AllocatePage();
            var data = new byte[storage.PageSize];
            data[11] = 0x7B;
            storage.WritePage(pageId, txn.TransactionId, data);

            txn.Prepare().ShouldBeTrue();
            txn.State.ShouldBe(TransactionState.Preparing);

            txn.Commit();
            txn.State.ShouldBe(TransactionState.Committed);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

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

    private static void CleanupFiles(string dbPath)
    {
        if (File.Exists(dbPath)) File.Delete(dbPath);

        string walPath = Path.ChangeExtension(dbPath, ".wal");
        if (File.Exists(walPath)) File.Delete(walPath);

        string altWalPath = dbPath + "-wal";
        if (File.Exists(altWalPath)) File.Delete(altWalPath);
    }
}