using System.Collections.Concurrent;
using System.Diagnostics;
using ZB.MOM.WW.CBDD.Core.CDC;
using ZB.MOM.WW.CBDD.Core.Transactions;
using ZB.MOM.WW.CBDD.Shared;

namespace ZB.MOM.WW.CBDD.Tests;

public class CdcTests : IDisposable
{
    private static readonly TimeSpan DefaultEventTimeout = TimeSpan.FromSeconds(3);
    private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(10);
    private readonly TestDbContext _db;

    private readonly string _dbPath = $"cdc_test_{Guid.NewGuid()}.db";

    /// <summary>
    ///     Initializes a new instance of the <see cref="CdcTests" /> class.
    /// </summary>
    public CdcTests()
    {
        _db = new TestDbContext(_dbPath);
    }

    /// <summary>
    ///     Disposes test resources and removes temporary files.
    /// </summary>
    public void Dispose()
    {
        _db.Dispose();
        if (File.Exists(_dbPath)) File.Delete(_dbPath);
        if (File.Exists(_dbPath + "-wal")) File.Delete(_dbPath + "-wal");
    }

    /// <summary>
    ///     Verifies that an insert operation publishes a CDC event.
    /// </summary>
    [Fact]
    public async Task Test_Cdc_Basic_Insert_Fires_Event()
    {
        var ct = TestContext.Current.CancellationToken;
        var events = new ConcurrentQueue<ChangeStreamEvent<int, Person>>();
        using var subscription = _db.People.Watch(true).Subscribe(events.Enqueue);

        var person = new Person { Id = 1, Name = "John", Age = 30 };
        _db.People.Insert(person);
        _db.SaveChanges();

        await WaitForEventCountAsync(events, 1, ct);

        var snapshot = events.ToArray();
        snapshot.Length.ShouldBe(1);
        snapshot[0].Type.ShouldBe(OperationType.Insert);
        snapshot[0].DocumentId.ShouldBe(1);
        snapshot[0].Entity.ShouldNotBeNull();
        snapshot[0].Entity!.Name.ShouldBe("John");
    }

    /// <summary>
    ///     Verifies payload is omitted when CDC capture payload is disabled.
    /// </summary>
    [Fact]
    public async Task Test_Cdc_No_Payload_When_Not_Requested()
    {
        var ct = TestContext.Current.CancellationToken;
        var events = new ConcurrentQueue<ChangeStreamEvent<int, Person>>();
        using var subscription = _db.People.Watch(false).Subscribe(events.Enqueue);

        var person = new Person { Id = 1, Name = "John", Age = 30 };
        _db.People.Insert(person);
        _db.SaveChanges();

        await WaitForEventCountAsync(events, 1, ct);

        var snapshot = events.ToArray();
        snapshot.Length.ShouldBe(1);
        snapshot[0].Entity.ShouldBeNull();
    }

    /// <summary>
    ///     Verifies CDC events are published only for committed changes.
    /// </summary>
    [Fact]
    public async Task Test_Cdc_Commit_Only()
    {
        var ct = TestContext.Current.CancellationToken;
        var events = new ConcurrentQueue<ChangeStreamEvent<int, Person>>();
        using var subscription = _db.People.Watch(true).Subscribe(events.Enqueue);

        using (var txn = _db.BeginTransaction())
        {
            _db.People.Insert(new Person { Id = 1, Name = "John" });
            events.Count.ShouldBe(0); // Not committed yet

            txn.Rollback();
        }

        await Task.Delay(100, ct);
        events.Count.ShouldBe(0); // Rolled back

        using (var txn = _db.BeginTransaction())
        {
            _db.People.Insert(new Person { Id = 2, Name = "Jane" });
            txn.Commit();
        }

        await WaitForEventCountAsync(events, 1, ct);
        var snapshot = events.ToArray();
        snapshot.Length.ShouldBe(1);
        snapshot[0].DocumentId.ShouldBe(2);
    }

    /// <summary>
    ///     Verifies update and delete operations publish CDC events.
    /// </summary>
    [Fact]
    public async Task Test_Cdc_Update_And_Delete()
    {
        var ct = TestContext.Current.CancellationToken;
        var events = new ConcurrentQueue<ChangeStreamEvent<int, Person>>();
        using var subscription = _db.People.Watch(true).Subscribe(events.Enqueue);

        var person = new Person { Id = 1, Name = "John", Age = 30 };
        _db.People.Insert(person);
        _db.SaveChanges();

        person.Name = "Johnny";
        _db.People.Update(person);
        _db.SaveChanges();

        _db.People.Delete(1);
        _db.SaveChanges();

        await WaitForEventCountAsync(events, 3, ct);

        var snapshot = events.ToArray();
        snapshot.Length.ShouldBe(3);
        snapshot[0].Type.ShouldBe(OperationType.Insert);
        snapshot[1].Type.ShouldBe(OperationType.Update);
        snapshot[2].Type.ShouldBe(OperationType.Delete);

        snapshot[1].Entity!.Name.ShouldBe("Johnny");
        snapshot[2].DocumentId.ShouldBe(1);
    }

    private static async Task WaitForEventCountAsync(
        ConcurrentQueue<ChangeStreamEvent<int, Person>> events,
        int expectedCount,
        CancellationToken ct)
    {
        var sw = Stopwatch.StartNew();
        while (sw.Elapsed < DefaultEventTimeout)
        {
            if (events.Count >= expectedCount) return;

            await Task.Delay(PollInterval, ct);
        }

        events.Count.ShouldBe(expectedCount);
    }
}

// Simple helper to avoid System.Reactive dependency in tests
public static class ObservableExtensions
{
    /// <summary>
    ///     Subscribes to an observable sequence using an action callback.
    /// </summary>
    /// <typeparam name="T">The event type.</typeparam>
    /// <param name="observable">The observable sequence.</param>
    /// <param name="onNext">The callback for next events.</param>
    /// <returns>An <see cref="IDisposable" /> subscription.</returns>
    public static IDisposable Subscribe<T>(this IObservable<T> observable, Action<T> onNext)
    {
        return observable.Subscribe(new AnonymousObserver<T>(onNext));
    }

    private class AnonymousObserver<T> : IObserver<T>
    {
        private readonly Action<T> _onNext;

        /// <summary>
        ///     Initializes a new instance of the <see cref="AnonymousObserver{T}" /> class.
        /// </summary>
        /// <param name="onNext">The callback for next events.</param>
        public AnonymousObserver(Action<T> onNext)
        {
            _onNext = onNext;
        }

        /// <summary>
        ///     Handles completion.
        /// </summary>
        public void OnCompleted()
        {
        }

        /// <summary>
        ///     Handles an observable error.
        /// </summary>
        /// <param name="error">The observed error.</param>
        public void OnError(Exception error)
        {
        }

        /// <summary>
        ///     Handles the next value.
        /// </summary>
        /// <param name="value">The observed value.</param>
        public void OnNext(T value)
        {
            _onNext(value);
        }
    }
}