using System.Linq;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;

namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Recorder;

/// <summary>
/// Durability + FIFO contract tests for the FasterLog-backed historization outbox. The
/// remove-then-reopen (restart durability) and drop-oldest (capacity) cases are load-bearing —
/// the outbox is the durable boundary the continuous-historization recorder acks against.
/// </summary>
public sealed class FasterLogHistorizationOutboxTests : IDisposable
{
    private readonly List<string> _dirs = new();

    private string NewTempDir()
    {
        var dir = Path.Combine(Path.GetTempPath(), "histgw-outbox-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(dir);
        _dirs.Add(dir);
        return dir;
    }

    private static HistorizationOutboxEntry E(string tag, double v) =>
        new(Guid.NewGuid(), tag, v, 192, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));

    [Fact]
    public async Task Append_then_peek_returns_fifo()
    {
        var dir = NewTempDir();
        using var o = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry);
        await o.AppendAsync(E("A", 1), TestContext.Current.CancellationToken);
        await o.AppendAsync(E("B", 2), TestContext.Current.CancellationToken);
        var batch = await o.PeekBatchAsync(10, TestContext.Current.CancellationToken);
        Assert.Equal(new[] { "A", "B" }, batch.Select(b => b.Tag));
        Assert.Equal(2, await o.CountAsync(TestContext.Current.CancellationToken));
    }

    [Fact]
    public async Task Remove_truncates_and_survives_restart()
    {
        var dir = NewTempDir();
        Guid keep;
        {
            using var o = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry);
            var a = E("A", 1);
            var b = E("B", 2);
            keep = b.Id;
            await o.AppendAsync(a, TestContext.Current.CancellationToken);
            await o.AppendAsync(b, TestContext.Current.CancellationToken);
            await o.PeekBatchAsync(10, TestContext.Current.CancellationToken);
            await o.RemoveAsync(a.Id, TestContext.Current.CancellationToken);   // ack A
        }

        using var reopened = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry);
        Assert.Equal(1, await reopened.CountAsync(TestContext.Current.CancellationToken));      // only B survives
        var batch = await reopened.PeekBatchAsync(10, TestContext.Current.CancellationToken);
        Assert.Equal(keep, batch[0].Id);
    }

    [Fact]
    public async Task Remove_last_entry_empties_and_survives_restart()
    {
        // Removing the only entry exercises the node.Next == null ⇒ TailAddress truncation branch:
        // the head advances to the tail, the outbox empties, and that empty state must persist across
        // a restart (a stale survivor here would silently re-drain an already-acked entry).
        var dir = NewTempDir();
        var a = E("A", 1);
        {
            using var o = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry);
            await o.AppendAsync(a, TestContext.Current.CancellationToken);
            await o.RemoveAsync(a.Id, TestContext.Current.CancellationToken);   // ack the only entry
            Assert.Equal(0, await o.CountAsync(TestContext.Current.CancellationToken));
        }

        using var reopened = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry);
        Assert.Equal(0, await reopened.CountAsync(TestContext.Current.CancellationToken));
        Assert.Empty(await reopened.PeekBatchAsync(10, TestContext.Current.CancellationToken));
    }

    [Fact]
    public async Task Remove_acks_fifo_up_to_and_including_target_and_survives_restart()
    {
        // FIFO implicit-ack: acking B truncates everything up to AND including B (so A is implicitly
        // acked too), leaving only the newer C. This is the documented head-advance semantics in
        // RemoveAsync, and it must persist across a restart.
        var dir = NewTempDir();
        var a = E("A", 1);
        var b = E("B", 2);
        var c = E("C", 3);
        {
            using var o = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry);
            await o.AppendAsync(a, TestContext.Current.CancellationToken);
            await o.AppendAsync(b, TestContext.Current.CancellationToken);
            await o.AppendAsync(c, TestContext.Current.CancellationToken);
            await o.RemoveAsync(b.Id, TestContext.Current.CancellationToken);   // acks A and B, leaves C

            Assert.Equal(1, await o.CountAsync(TestContext.Current.CancellationToken));
            var remaining = await o.PeekBatchAsync(10, TestContext.Current.CancellationToken);
            Assert.Equal(new[] { c.Id }, remaining.Select(e => e.Id));
        }

        using var reopened = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry);
        Assert.Equal(1, await reopened.CountAsync(TestContext.Current.CancellationToken));
        var survived = await reopened.PeekBatchAsync(10, TestContext.Current.CancellationToken);
        Assert.Equal(new[] { c.Id }, survived.Select(e => e.Id));
    }

    [Fact]
    public async Task Capacity_full_drops_oldest_and_counts()
    {
        var dir = NewTempDir();
        using var o = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry, capacity: 2);
        await o.AppendAsync(E("A", 1), TestContext.Current.CancellationToken);
        await o.AppendAsync(E("B", 2), TestContext.Current.CancellationToken);
        await o.AppendAsync(E("C", 3), TestContext.Current.CancellationToken);  // overflow -> drop oldest (A)
        Assert.Equal(2, await o.CountAsync(TestContext.Current.CancellationToken));
        Assert.Equal(1, o.DroppedCount);
        var tags = (await o.PeekBatchAsync(10, TestContext.Current.CancellationToken)).Select(b => b.Tag).ToArray();
        Assert.DoesNotContain("A", tags);
    }

    [Fact]
    public async Task Periodic_mode_commits_and_recovers()
    {
        var dir = NewTempDir();
        var a = E("A", 1);
        var b = E("B", 2);
        {
            using var o = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.Periodic, commitIntervalMs: 20);
            await o.AppendAsync(a, TestContext.Current.CancellationToken);
            await o.AppendAsync(b, TestContext.Current.CancellationToken);
            // Dispose flushes a final commit, making the periodic-mode appends durable.
        }

        using var reopened = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.Periodic, commitIntervalMs: 20);
        Assert.Equal(2, await reopened.CountAsync(TestContext.Current.CancellationToken));
        var batch = await reopened.PeekBatchAsync(10, TestContext.Current.CancellationToken);
        Assert.Equal(new[] { a.Id, b.Id }, batch.Select(e => e.Id));
    }

    [Fact]
    public async Task Remove_unknown_id_is_noop()
    {
        var dir = NewTempDir();
        using var o = new FasterLogHistorizationOutbox(dir, HistorizationCommitMode.PerEntry);
        await o.AppendAsync(E("A", 1), TestContext.Current.CancellationToken);
        await o.RemoveAsync(Guid.NewGuid(), TestContext.Current.CancellationToken); // never appended -> no-op
        Assert.Equal(1, await o.CountAsync(TestContext.Current.CancellationToken));
    }

    public void Dispose()
    {
        foreach (var dir in _dirs)
        {
            try
            {
                Directory.Delete(dir, recursive: true);
            }
            catch (IOException)
            {
                // Best-effort cleanup; a lingering OS handle must not fail the test run.
            }
            catch (UnauthorizedAccessException)
            {
                // Best-effort cleanup.
            }
        }
    }
}
