using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Core.Transactions;
using ZB.MOM.WW.CBDD.Shared;
using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers;
using System.Buffers;
using Xunit;

namespace ZB.MOM.WW.CBDD.Tests;

public class WalIndexTests : IDisposable
{
    private readonly string _dbPath;
    private readonly string _walPath;
    private readonly Shared.TestDbContext _db;
    private readonly ITestOutputHelper _output;

    /// <summary>
    /// Initializes a new instance of the <see cref="WalIndexTests"/> class.
    /// </summary>
    /// <param name="output">Test output sink.</param>
    public WalIndexTests(ITestOutputHelper output)
    {
        _output = output;
        _dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_index_{Guid.NewGuid()}.db");
        // WAL defaults to .wal next to db
        _walPath = Path.ChangeExtension(_dbPath, ".wal");

        _db = new Shared.TestDbContext(_dbPath);
    }

    /// <summary>
    /// Verifies index writes are recorded in the WAL.
    /// </summary>
    [Fact]
    public void IndexWritesAreLoggedToWal()
    {
        // 2. Start a transaction
        using var txn = _db.BeginTransaction();
        _output.WriteLine($"Started Transaction: {txn.TransactionId}");

        // 3. Insert a user
        var user = new User { Name = "Alice", Age = 30 };
        _db.Users.Insert(user);

        // 4. Commit
        txn.Commit();
        _output.WriteLine("Committed Transaction");

        // 5. Verify WAL
        // Dispose current storage to release file locks, BUT skip checkpoint/truncate
        _db.Dispose();

        File.Exists(_walPath).ShouldBeTrue("WAL file should exist");

        using var walReader = new WriteAheadLog(_walPath);
        var records = walReader.ReadAll();

        _output.WriteLine($"Found {records.Count} WAL records");

        // Filter for this transaction
        var txnRecords = records.Where(r => r.TransactionId == txn.TransactionId).ToList();

        txnRecords.ShouldContain(r => r.Type == WalRecordType.Begin);
        txnRecords.ShouldContain(r => r.Type == WalRecordType.Commit);

        var writeRecords = txnRecords.Where(r => r.Type == WalRecordType.Write).ToList();
        _output.WriteLine($"Found {writeRecords.Count} Write records for Txn {txn.TransactionId}");

        // Analyze pages
        int indexPageCount = 0;
        int dataPageCount = 0;

        foreach (var record in writeRecords)
        {
            var pageType = ParsePageType(record.AfterImage);
            _output.WriteLine($"Page {record.PageId}: Type={pageType}, Size={record.AfterImage?.Length}");

            if (pageType == PageType.Index) indexPageCount++;
            else if (pageType == PageType.Data) dataPageCount++;
        }

        (indexPageCount > 0).ShouldBeTrue($"Expected at least 1 Index page write, found {indexPageCount}");
        (dataPageCount > 0).ShouldBeTrue($"Expected at least 1 Data page write, found {dataPageCount}");
    }

    private PageType ParsePageType(byte[]? pageData)
    {
        if (pageData == null || pageData.Length < 32) return (PageType)0;
        // PageType is at offset 4 (1 byte)
        return (PageType)pageData[4]; // Casting byte to PageType
    }

    /// <summary>
    /// Verifies offline compaction leaves the WAL empty.
    /// </summary>
    [Fact]
    public void Compact_ShouldLeaveWalEmpty_AfterOfflineRun()
    {
        for (var i = 0; i < 100; i++)
        {
            _db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i % 30 });
        }

        _db.SaveChanges();
        _db.Storage.GetWalSize().ShouldBeGreaterThan(0);

        var stats = _db.Compact(new CompactionOptions
        {
            EnableTailTruncation = true,
            NormalizeFreeList = true,
            DefragmentSlottedPages = true
        });

        stats.OnlineMode.ShouldBeFalse();
        _db.Storage.GetWalSize().ShouldBe(0);
        new FileInfo(_walPath).Length.ShouldBe(0);
    }

    /// <summary>
    /// Verifies WAL recovery followed by compaction preserves data.
    /// </summary>
    [Fact]
    public void Recover_WithCommittedWal_ThenCompact_ShouldPreserveData()
    {
        var dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_recover_compact_{Guid.NewGuid():N}.db");
        var walPath = Path.ChangeExtension(dbPath, ".wal");
        var markerPath = $"{dbPath}.compact.state";
        var expectedIds = new List<ObjectId>();

        try
        {
            using (var writer = new Shared.TestDbContext(dbPath))
            {
                for (var i = 0; i < 48; i++)
                {
                    expectedIds.Add(writer.Users.Insert(new User { Name = $"recover-{i:D3}", Age = i % 10 }));
                }

                writer.SaveChanges();
                writer.Storage.GetWalSize().ShouldBeGreaterThan(0);
            }

            new FileInfo(walPath).Length.ShouldBeGreaterThan(0);

            using (var recovered = new Shared.TestDbContext(dbPath))
            {
                recovered.Users.Count().ShouldBe(expectedIds.Count);
                recovered.Compact();
                recovered.Storage.GetWalSize().ShouldBe(0);

                foreach (var id in expectedIds)
                {
                    recovered.Users.FindById(id).ShouldNotBeNull();
                }
            }
        }
        finally
        {
            if (File.Exists(dbPath)) File.Delete(dbPath);
            if (File.Exists(walPath)) File.Delete(walPath);
            if (File.Exists(markerPath)) File.Delete(markerPath);
        }
    }

    /// <summary>
    /// Releases test resources.
    /// </summary>
    public void Dispose()
    {
        try
        {
            _db?.Dispose(); // Safe to call multiple times
        }
        catch { }

        try { if (File.Exists(_dbPath)) File.Delete(_dbPath); } catch { }
        try { if (File.Exists(_walPath)) File.Delete(_walPath); } catch { }
    }
}
