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;
using System.Security.Cryptography;
using System.IO.Compression;
using System.IO.MemoryMappedFiles;

namespace ZB.MOM.WW.CBDD.Tests;

public class DbContextTests : IDisposable
{
    private string _dbPath;

    /// <summary>
    /// Initializes test file paths for database context tests.
    /// </summary>
    public DbContextTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_{Guid.NewGuid()}.db");
    }

    /// <summary>
    /// Verifies the basic database context lifecycle works.
    /// </summary>
    [Fact]
    public void DbContext_BasicLifecycle_Works()
    {
        using var db = new Shared.TestDbContext(_dbPath);

        var user = new User { Name = "Alice", Age = 30 };
        var id = db.Users.Insert(user);

        var found = db.Users.FindById(id);
        found.ShouldNotBeNull();
        found.Name.ShouldBe("Alice");
        found.Age.ShouldBe(30);
    }

    /// <summary>
    /// Verifies multiple CRUD operations execute correctly in one context.
    /// </summary>
    [Fact]
    public void DbContext_MultipleOperations_Work()
    {
        using var db = new Shared.TestDbContext(_dbPath);

        // Insert
        var alice = new User { Name = "Alice", Age = 30 };
        var bob = new User { Name = "Bob", Age = 25 };

        var id1 = db.Users.Insert(alice);
        var id2 = db.Users.Insert(bob);

        // FindAll
        var all = db.Users.FindAll().ToList();
        all.Count.ShouldBe(2);

        // Update
        alice.Age = 31;
        db.Users.Update(alice).ShouldBeTrue();

        var updated = db.Users.FindById(id1);
        updated!.Age.ShouldBe(31);

        // Delete
        db.Users.Delete(id2).ShouldBeTrue();
        db.Users.Count().ShouldBe(1);
    }

    /// <summary>
    /// Verifies disposing and reopening context preserves persisted data.
    /// </summary>
    [Fact]
    public void DbContext_Dispose_ReleasesResources()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_reopen.db");
        var totalUsers = 0;
        // First context - insert and dispose (auto-checkpoint)
        using (var db = new Shared.TestDbContext(_dbPath))
        {
            db.Users.Insert(new User { Name = "Test", Age = 20 });
            db.SaveChanges(); // Explicitly save changes to ensure data is in WAL
            var beforeCheckpointTotalUsers = db.Users.FindAll().Count();
            db.ForceCheckpoint(); // Force checkpoint to ensure data is persisted to main file
            totalUsers = db.Users.FindAll().Count();
            var countedUsers = db.Users.Count();
            totalUsers.ShouldBe(beforeCheckpointTotalUsers);
        } // Dispose → Commit → ForceCheckpoint → Write to PageFile

        // Should be able to open again and see persisted data
        using var db2 = new Shared.TestDbContext(_dbPath);

        totalUsers.ShouldBe(1);
        db2.Users.FindAll().Count().ShouldBe(totalUsers);
        db2.Users.Count().ShouldBe(totalUsers);
    }
    private static string ComputeFileHash(string path)
    {
        using var stream = File.OpenRead(path);
        using var sha256 = SHA256.Create();
        return Convert.ToHexString(sha256.ComputeHash(stream));
    }

    /// <summary>
    /// Verifies database file size and content change after insert and checkpoint.
    /// </summary>
    [Fact]
    public void DatabaseFile_SizeAndContent_ChangeAfterInsert()
    {
        var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbfile_{Guid.NewGuid()}.db");

        // 1. Crea e chiudi database vuoto
        using (var db = new Shared.TestDbContext(dbPath))
        {
            db.Users.Insert(new User { Name = "Pippo", Age = 42 });
        }
        var initialSize = new FileInfo(dbPath).Length;
        var initialHash = ComputeFileHash(dbPath);

        // 2. Riapri, inserisci, chiudi
        using (var db = new Shared.TestDbContext(dbPath))
        {
            db.Users.Insert(new User { Name = "Test", Age = 42 });
            db.ForceCheckpoint(); // Forza persistenza
        }
        var afterInsertSize = new FileInfo(dbPath).Length;
        var afterInsertHash = ComputeFileHash(dbPath);

        // 3. Verifica che dimensione e hash siano cambiati
        afterInsertSize.ShouldNotBe(initialSize);
        afterInsertHash.ShouldNotBe(initialHash);
    }

    /// <summary>
    /// Verifies the WAL file path is auto-derived from database path.
    /// </summary>
    [Fact]
    public void DbContext_AutoDerivesWalPath()
    {
        using var db = new Shared.TestDbContext(_dbPath);
        db.Users.Insert(new User { Name = "Test", Age = 20 });

        var walPath = Path.ChangeExtension(_dbPath, ".wal");
        File.Exists(walPath).ShouldBeTrue();
    }

    /// <summary>
    /// Verifies custom page file and compression options support roundtrip data access.
    /// </summary>
    [Fact]
    public void DbContext_WithCustomPageFileAndCompressionOptions_ShouldSupportRoundTrip()
    {
        var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_compression_{Guid.NewGuid():N}.db");
        var options = new CompressionOptions
        {
            EnableCompression = true,
            MinSizeBytes = 0,
            MinSavingsPercent = 0,
            Codec = CompressionCodec.Brotli,
            Level = CompressionLevel.Fastest
        };

        var config = new PageFileConfig
        {
            PageSize = 16 * 1024,
            InitialFileSize = 1024 * 1024,
            Access = MemoryMappedFileAccess.ReadWrite
        };

        try
        {
            using var db = new Shared.TestDbContext(dbPath, config, options);
            var payload = string.Concat(Enumerable.Repeat("compressible-", 3000));
            var id = db.Users.Insert(new User { Name = payload, Age = 77 });
            db.SaveChanges();

            var loaded = db.Users.FindById(id);
            loaded.ShouldNotBeNull();
            loaded.Name.ShouldBe(payload);
            db.GetCompressionStats().CompressedDocumentCount.ShouldBeGreaterThanOrEqualTo(1);
        }
        finally
        {
            CleanupDbFiles(dbPath);
        }
    }

    /// <summary>
    /// Verifies compact API returns stats and preserves data consistency.
    /// </summary>
    [Fact]
    public void DbContext_CompactApi_ShouldReturnStatsAndPreserveData()
    {
        var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_compact_{Guid.NewGuid():N}.db");
        try
        {
            using var db = new Shared.TestDbContext(dbPath);
            for (var i = 0; i < 120; i++)
            {
                db.Users.Insert(new User { Name = $"compact-{i:D3}", Age = i % 20 });
            }

            db.SaveChanges();
            db.Users.Count().ShouldBe(120);
            db.SaveChanges();

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

            stats.OnlineMode.ShouldBeFalse();
            db.Users.Count().ShouldBe(120);
        }
        finally
        {
            CleanupDbFiles(dbPath);
        }
    }

    /// <summary>
    /// Disposes test resources and cleans up generated files.
    /// </summary>
    public void Dispose()
    {
        try
        {
            CleanupDbFiles(_dbPath);
        }
        catch
        {
            // Ignore cleanup errors
        }
    }

    private static void CleanupDbFiles(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);
    }
}
