using System.Reflection;
using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Core.Transactions;
using ZB.MOM.WW.CBDD.Shared;

namespace ZB.MOM.WW.CBDD.Tests;

public class CheckpointModeTests
{
    /// <summary>
    /// Verifies default checkpoint mode truncates WAL.
    /// </summary>
    [Fact]
    public void Checkpoint_Default_ShouldUseTruncate()
    {
        var dbPath = NewDbPath();
        try
        {
            using var db = new TestDbContext(dbPath);
            db.Users.Insert(new User { Name = "checkpoint-default", Age = 42 });
            db.SaveChanges();

            db.Storage.GetWalSize().ShouldBeGreaterThan(0);
            var result = db.Checkpoint();

            result.Mode.ShouldBe(CheckpointMode.Truncate);
            result.Executed.ShouldBeTrue();
            result.Truncated.ShouldBeTrue();
            db.Storage.GetWalSize().ShouldBe(0);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    /// Verifies passive mode skips when checkpoint lock is contended.
    /// </summary>
    [Fact]
    public void Checkpoint_Passive_ShouldSkip_WhenLockIsContended()
    {
        var dbPath = NewDbPath();
        try
        {
            using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
            var gate = GetCommitGate(storage);
            gate.Wait(TestContext.Current.CancellationToken);
            try
            {
                var result = storage.Checkpoint(CheckpointMode.Passive);
                result.Mode.ShouldBe(CheckpointMode.Passive);
                result.Executed.ShouldBeFalse();
                result.Truncated.ShouldBeFalse();
                result.Restarted.ShouldBeFalse();
            }
            finally
            {
                gate.Release();
            }
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    /// Verifies full checkpoint applies data and appends a checkpoint marker without truncating WAL.
    /// </summary>
    [Fact]
    public void Checkpoint_Full_ShouldAppendMarker_AndPreserveWal()
    {
        var dbPath = NewDbPath();
        var walPath = Path.ChangeExtension(dbPath, ".wal");

        try
        {
            using (var db = new TestDbContext(dbPath))
            {
                db.Users.Insert(new User { Name = "checkpoint-full", Age = 50 });
                db.SaveChanges();

                var walBefore = db.Storage.GetWalSize();
                walBefore.ShouldBeGreaterThan(0);

                var result = db.Checkpoint(CheckpointMode.Full);
                result.Mode.ShouldBe(CheckpointMode.Full);
                result.Executed.ShouldBeTrue();
                result.Truncated.ShouldBeFalse();
                result.WalBytesAfter.ShouldBeGreaterThan(0);
                db.Storage.GetWalSize().ShouldBeGreaterThan(0);
            }

            using var wal = new WriteAheadLog(walPath);
            wal.ReadAll().Any(r => r.Type == WalRecordType.Checkpoint).ShouldBeTrue();
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    /// Verifies restart checkpoint clears WAL and allows subsequent writes.
    /// </summary>
    [Fact]
    public void Checkpoint_Restart_ShouldResetWal_AndAcceptNewWrites()
    {
        var dbPath = NewDbPath();
        try
        {
            using var db = new TestDbContext(dbPath);
            db.Users.Insert(new User { Name = "restart-before", Age = 30 });
            db.SaveChanges();

            db.Storage.GetWalSize().ShouldBeGreaterThan(0);
            var result = db.Checkpoint(CheckpointMode.Restart);
            result.Mode.ShouldBe(CheckpointMode.Restart);
            result.Executed.ShouldBeTrue();
            result.Truncated.ShouldBeTrue();
            result.Restarted.ShouldBeTrue();
            db.Storage.GetWalSize().ShouldBe(0);

            db.Users.Insert(new User { Name = "restart-after", Age = 31 });
            db.SaveChanges();
            db.Storage.GetWalSize().ShouldBeGreaterThan(0);
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    /// Verifies recovery remains deterministic after a full checkpoint boundary.
    /// </summary>
    [Fact]
    public void Recover_AfterFullCheckpoint_ShouldApplyLatestCommitDeterministically()
    {
        var dbPath = NewDbPath();
        try
        {
            uint pageId;

            using (var storage = new StorageEngine(dbPath, PageFileConfig.Default))
            {
                pageId = storage.AllocatePage();

                using (var tx1 = storage.BeginTransaction())
                {
                    var first = new byte[storage.PageSize];
                    first[0] = 1;
                    storage.WritePage(pageId, tx1.TransactionId, first);
                    tx1.Commit();
                }

                storage.Checkpoint(CheckpointMode.Full);

                using (var tx2 = storage.BeginTransaction())
                {
                    var second = new byte[storage.PageSize];
                    second[0] = 2;
                    storage.WritePage(pageId, tx2.TransactionId, second);
                    tx2.Commit();
                }
            }

            using (var recovered = new StorageEngine(dbPath, PageFileConfig.Default))
            {
                var buffer = new byte[recovered.PageSize];
                recovered.ReadPage(pageId, 0, buffer);
                buffer[0].ShouldBe((byte)2);
                recovered.GetWalSize().ShouldBe(0);
            }
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    /// <summary>
    /// Verifies asynchronous mode-based checkpoints return expected result metadata.
    /// </summary>
    [Fact]
    public async Task CheckpointAsync_Full_ShouldReturnResult()
    {
        var dbPath = NewDbPath();
        try
        {
            using var db = new TestDbContext(dbPath);
            db.Users.Insert(new User { Name = "checkpoint-async", Age = 38 });
            db.SaveChanges();

            var result = await db.CheckpointAsync(CheckpointMode.Full, TestContext.Current.CancellationToken);
            result.Mode.ShouldBe(CheckpointMode.Full);
            result.Executed.ShouldBeTrue();
            result.Truncated.ShouldBeFalse();
        }
        finally
        {
            CleanupFiles(dbPath);
        }
    }

    private static SemaphoreSlim GetCommitGate(StorageEngine storage)
    {
        var field = typeof(StorageEngine).GetField("_commitLock", BindingFlags.Instance | BindingFlags.NonPublic);
        field.ShouldNotBeNull();
        return (SemaphoreSlim)field!.GetValue(storage)!;
    }

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

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

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

        var markerPath = $"{dbPath}.compact.state";
        if (File.Exists(markerPath)) File.Delete(markerPath);
    }
}
