using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;

/// <summary>
///     Tests for <see cref="GatewayGalaxyDataWriter.InvalidateHandleCaches"/>.
///     The SDK session types are sealed with internal ctors and cannot be faked, so we
///     drive the cache-seeding path through
///     <see cref="GatewayGalaxyDataWriter.SeedHandleCachesForTest"/> and verify the
///     handle-count seams — the contract under test is purely that
///     <see cref="GatewayGalaxyDataWriter.InvalidateHandleCaches"/> zeroes both dictionaries
///     so the next write is forced to re-AddItem + re-AdviseSupervisory.
/// </summary>
public sealed class GatewayGalaxyDataWriterTests
{
    private static GalaxyMxSession MinimalSession()
        => new(new GalaxyMxAccessOptions(ClientName: "OtOpcUa-Test"));

    /// <summary>
    ///     Approach (b): seed the item-handle cache directly via the internal test seam,
    ///     confirm the count is positive, call <see cref="GatewayGalaxyDataWriter.InvalidateHandleCaches"/>,
    ///     and confirm both caches are cleared.
    ///     The next write (not simulated here — needs a live gw) would therefore be forced
    ///     to re-AddItem because the cache is empty.
    /// </summary>
    [Fact]
    public void InvalidateHandleCaches_clears_item_and_supervised_handle_caches()
    {
        var session = MinimalSession();
        var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);

        // Pre-seed both caches via the internal test seam so we can assert the
        // "after a write" state without spinning up a real gRPC gateway session.
        writer.SeedHandleCachesForTest("TestMachine_001.TestAttr", itemHandle: 42, supervised: true);

        writer.CachedItemHandleCount.ShouldBe(1);
        writer.CachedSupervisedHandleCount.ShouldBe(1);

        writer.InvalidateHandleCaches();

        writer.CachedItemHandleCount.ShouldBe(0);
        writer.CachedSupervisedHandleCount.ShouldBe(0);
    }

    /// <summary>
    ///     A second seed + invalidate cycle proves the method isn't one-shot — a reconnect
    ///     followed by writes followed by another reconnect must also start fresh.
    /// </summary>
    [Fact]
    public void InvalidateHandleCaches_is_repeatable_across_multiple_reconnects()
    {
        var session = MinimalSession();
        var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);

        // First session cycle
        writer.SeedHandleCachesForTest("Tag.A", itemHandle: 1, supervised: false);
        writer.SeedHandleCachesForTest("Tag.B", itemHandle: 2, supervised: true);
        writer.CachedItemHandleCount.ShouldBe(2);
        writer.InvalidateHandleCaches();
        writer.CachedItemHandleCount.ShouldBe(0);
        writer.CachedSupervisedHandleCount.ShouldBe(0);

        // Second session cycle — handles re-populated after the reconnect's replay
        writer.SeedHandleCachesForTest("Tag.A", itemHandle: 99, supervised: true);
        writer.CachedItemHandleCount.ShouldBe(1);
        writer.InvalidateHandleCaches();
        writer.CachedItemHandleCount.ShouldBe(0);
    }

    /// <summary>
    ///     <see cref="GatewayGalaxyDataWriter.InvalidateHandleCaches"/> on a fresh (never-used)
    ///     writer must be a no-op rather than throwing — the reconnect supervisor may call it
    ///     before any write has occurred.
    /// </summary>
    [Fact]
    public void InvalidateHandleCaches_on_empty_caches_is_a_noop()
    {
        var session = MinimalSession();
        var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);

        // Caches are empty — must not throw.
        writer.CachedItemHandleCount.ShouldBe(0);
        writer.CachedSupervisedHandleCount.ShouldBe(0);
        writer.InvalidateHandleCaches();
        writer.CachedItemHandleCount.ShouldBe(0);
        writer.CachedSupervisedHandleCount.ShouldBe(0);
    }

    // ===== subscribedHandleSource / TryResolveCachedOrBorrowed tests =====

    /// <summary>
    ///     When a <c>subscribedHandleSource</c> delegate returns a positive handle for the
    ///     requested ref, <see cref="GatewayGalaxyDataWriter.TryResolveCachedOrBorrowed"/> must
    ///     return that handle. Critically, the borrowed handle must NOT be stored in the writer's
    ///     own item-handle cache (the subscription registry owns its lifecycle), and
    ///     <see cref="GatewayGalaxyDataWriter.AddItemCallCount"/> must remain zero (no real
    ///     gateway round-trip issued).
    /// </summary>
    [Fact]
    public void TryResolveCachedOrBorrowed_returns_borrowed_handle_without_caching_it()
    {
        var writer = new GatewayGalaxyDataWriter(
            MinimalSession(), writeUserId: 0, logger: null,
            subscribedHandleSource: fr => fr == "Tag.X" ? 7 : (int?)null);

        var result = writer.TryResolveCachedOrBorrowed("Tag.X");

        result.ShouldBe(7);
        writer.CachedItemHandleCount.ShouldBe(0);   // borrow NOT stored in _itemHandles
        writer.AddItemCallCount.ShouldBe(0);         // no real AddItem issued
    }

    /// <summary>
    ///     When a handle is present in the writer's own <c>_itemHandles</c> cache (seeded via
    ///     <see cref="GatewayGalaxyDataWriter.SeedHandleCachesForTest"/>), that cached handle must
    ///     win over the <c>subscribedHandleSource</c> delegate — the writer's own earlier AddItem
    ///     takes priority.
    /// </summary>
    [Fact]
    public void TryResolveCachedOrBorrowed_cached_handle_wins_over_source()
    {
        // Source would return 99 for "Tag.Y", but the writer already cached handle 42 for it.
        var writer = new GatewayGalaxyDataWriter(
            MinimalSession(), writeUserId: 0, logger: null,
            subscribedHandleSource: fr => fr == "Tag.Y" ? 99 : (int?)null);

        writer.SeedHandleCachesForTest("Tag.Y", itemHandle: 42, supervised: false);

        var result = writer.TryResolveCachedOrBorrowed("Tag.Y");

        result.ShouldBe(42);  // cached wins, not 99 from the source
    }

    /// <summary>
    ///     When no <c>subscribedHandleSource</c> is provided (<c>null</c>), the method must
    ///     return <c>null</c> for a ref that has never been cached — preserving today's
    ///     AddItem-required behavior.
    /// </summary>
    [Fact]
    public void TryResolveCachedOrBorrowed_null_source_returns_null_when_not_cached()
    {
        var writer = new GatewayGalaxyDataWriter(MinimalSession(), writeUserId: 0);

        var result = writer.TryResolveCachedOrBorrowed("Tag.Z");

        result.ShouldBeNull();
    }

    /// <summary>
    ///     A <c>subscribedHandleSource</c> that returns 0 (a sentinel meaning "subscribe
    ///     pending / failed") must be treated as no borrow — the method returns <c>null</c>
    ///     so the caller issues a fresh AddItem.
    /// </summary>
    [Fact]
    public void TryResolveCachedOrBorrowed_source_returning_zero_treated_as_no_borrow()
    {
        var writer = new GatewayGalaxyDataWriter(
            MinimalSession(), writeUserId: 0, logger: null,
            subscribedHandleSource: _ => 0);

        var result = writer.TryResolveCachedOrBorrowed("Tag.W");

        result.ShouldBeNull();
    }

    /// <summary>
    ///     A <c>subscribedHandleSource</c> that returns a negative value (another failure
    ///     sentinel) must also be treated as no borrow — the method returns <c>null</c>.
    /// </summary>
    [Fact]
    public void TryResolveCachedOrBorrowed_source_returning_negative_treated_as_no_borrow()
    {
        var writer = new GatewayGalaxyDataWriter(
            MinimalSession(), writeUserId: 0, logger: null,
            subscribedHandleSource: _ => -5);

        var result = writer.TryResolveCachedOrBorrowed("Tag.V");

        result.ShouldBeNull();
    }
}
