using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;

namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;

[Trait("Category", "Unit")]
public sealed class LiteDbConfigCacheTests : IDisposable
{
    private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"otopcua-cache-test-{Guid.NewGuid():N}.db");

    public void Dispose()
    {
        if (File.Exists(_dbPath)) File.Delete(_dbPath);
    }

    private GenerationSnapshot Snapshot(string cluster, long gen) => new()
    {
        ClusterId = cluster,
        GenerationId = gen,
        CachedAt = DateTime.UtcNow,
        PayloadJson = $"{{\"g\":{gen}}}",
    };

    [Fact]
    public async Task Roundtrip_preserves_payload()
    {
        using var cache = new LiteDbConfigCache(_dbPath);
        var put = Snapshot("c-1", 42);
        await cache.PutAsync(put);

        var got = await cache.GetMostRecentAsync("c-1");
        got.ShouldNotBeNull();
        got!.GenerationId.ShouldBe(42);
        got.PayloadJson.ShouldBe(put.PayloadJson);
    }

    [Fact]
    public async Task GetMostRecent_returns_latest_when_multiple_generations_present()
    {
        using var cache = new LiteDbConfigCache(_dbPath);
        foreach (var g in new long[] { 10, 20, 15 })
            await cache.PutAsync(Snapshot("c-1", g));

        var got = await cache.GetMostRecentAsync("c-1");
        got!.GenerationId.ShouldBe(20);
    }

    [Fact]
    public async Task GetMostRecent_returns_null_for_unknown_cluster()
    {
        using var cache = new LiteDbConfigCache(_dbPath);
        (await cache.GetMostRecentAsync("ghost")).ShouldBeNull();
    }

    [Fact]
    public async Task Prune_keeps_latest_N_and_drops_older()
    {
        using var cache = new LiteDbConfigCache(_dbPath);
        for (long g = 1; g <= 15; g++)
            await cache.PutAsync(Snapshot("c-1", g));

        await cache.PruneOldGenerationsAsync("c-1", keepLatest: 10);

        (await cache.GetMostRecentAsync("c-1"))!.GenerationId.ShouldBe(15);

        // Drop them one by one and count — should be exactly 10 remaining
        var count = 0;
        while (await cache.GetMostRecentAsync("c-1") is not null)
        {
            count++;
            await cache.PruneOldGenerationsAsync("c-1", keepLatest: Math.Max(0, 10 - count));
            if (count > 20) break; // safety
        }
        count.ShouldBe(10);
    }

    [Fact]
    public async Task Put_same_cluster_generation_twice_replaces_not_duplicates()
    {
        using var cache = new LiteDbConfigCache(_dbPath);
        var first = Snapshot("c-1", 1);
        first.PayloadJson = "{\"v\":1}";
        await cache.PutAsync(first);

        var second = Snapshot("c-1", 1);
        second.PayloadJson = "{\"v\":2}";
        await cache.PutAsync(second);

        (await cache.GetMostRecentAsync("c-1"))!.PayloadJson.ShouldBe("{\"v\":2}");
    }

    [Fact]
    public void Corrupt_file_surfaces_as_LocalConfigCacheCorruptException()
    {
        // Write a file large enough to look like a LiteDB page but with garbage contents so page
        // deserialization fails on the first read probe.
        File.WriteAllBytes(_dbPath, new byte[8192]);
        Array.Fill<byte>(File.ReadAllBytes(_dbPath), 0xAB);
        using (var fs = File.OpenWrite(_dbPath))
        {
            fs.Write(new byte[8192].Select(_ => (byte)0xAB).ToArray());
        }

        Should.Throw<LocalConfigCacheCorruptException>(() => new LiteDbConfigCache(_dbPath));
    }
}
