using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;

[Trait("Category", "Unit")]
public sealed class CapabilityInvokerTests
{
    private static CapabilityInvoker MakeInvoker(
        DriverResiliencePipelineBuilder builder,
        DriverResilienceOptions options) =>
        new(builder, "drv-test", () => options);

    /// <summary>Verifies that the capability invoker returns the value from the call site.</summary>
    [Fact]
    public async Task Read_ReturnsValue_FromCallSite()
    {
        var invoker = MakeInvoker(new DriverResiliencePipelineBuilder(), new DriverResilienceOptions { Tier = DriverTier.A });

        var result = await invoker.ExecuteAsync(
            DriverCapability.Read,
            "host-1",
            _ => ValueTask.FromResult(42),
            CancellationToken.None);

        result.ShouldBe(42);
    }

    /// <summary>Verifies that the capability invoker retries on transient failures.</summary>
    [Fact]
    public async Task Read_Retries_OnTransientFailure()
    {
        var invoker = MakeInvoker(new DriverResiliencePipelineBuilder(), new DriverResilienceOptions { Tier = DriverTier.A });
        var attempts = 0;

        var result = await invoker.ExecuteAsync(
            DriverCapability.Read,
            "host-1",
            async _ =>
            {
                attempts++;
                if (attempts < 2) throw new InvalidOperationException("transient");
                await Task.Yield();
                return "ok";
            },
            CancellationToken.None);

        result.ShouldBe("ok");
        attempts.ShouldBe(2);
    }

    /// <summary>Verifies that non-idempotent writes do not retry even when the policy has retries configured.</summary>
    [Fact]
    public async Task Write_NonIdempotent_DoesNotRetry_EvenWhenPolicyHasRetries()
    {
        var options = new DriverResilienceOptions
        {
            Tier = DriverTier.A,
            CapabilityPolicies = new Dictionary<DriverCapability, CapabilityPolicy>
            {
                [DriverCapability.Write] = new(TimeoutSeconds: 2, RetryCount: 3, BreakerFailureThreshold: 5),
            },
        };
        var invoker = MakeInvoker(new DriverResiliencePipelineBuilder(), options);
        var attempts = 0;

        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await invoker.ExecuteWriteAsync(
                "host-1",
                isIdempotent: false,
                async _ =>
                {
                    attempts++;
                    await Task.Yield();
                    throw new InvalidOperationException("boom");
#pragma warning disable CS0162
                    return 0;
#pragma warning restore CS0162
                },
                CancellationToken.None));

        attempts.ShouldBe(1, "non-idempotent write must never replay");
    }

    /// <summary>Verifies that idempotent writes retry when the policy has retries configured.</summary>
    [Fact]
    public async Task Write_Idempotent_Retries_WhenPolicyHasRetries()
    {
        var options = new DriverResilienceOptions
        {
            Tier = DriverTier.A,
            CapabilityPolicies = new Dictionary<DriverCapability, CapabilityPolicy>
            {
                [DriverCapability.Write] = new(TimeoutSeconds: 2, RetryCount: 3, BreakerFailureThreshold: 5),
            },
        };
        var invoker = MakeInvoker(new DriverResiliencePipelineBuilder(), options);
        var attempts = 0;

        var result = await invoker.ExecuteWriteAsync(
            "host-1",
            isIdempotent: true,
            async _ =>
            {
                attempts++;
                if (attempts < 2) throw new InvalidOperationException("transient");
                await Task.Yield();
                return "ok";
            },
            CancellationToken.None);

        result.ShouldBe("ok");
        attempts.ShouldBe(2);
    }

    /// <summary>Verifies that writes do not retry when the policy has zero retries configured.</summary>
    [Fact]
    public async Task Write_Default_DoesNotRetry_WhenPolicyHasZeroRetries()
    {
        // Tier A Write default is RetryCount=0. Even isIdempotent=true shouldn't retry
        // because the policy says not to.
        var invoker = MakeInvoker(new DriverResiliencePipelineBuilder(), new DriverResilienceOptions { Tier = DriverTier.A });
        var attempts = 0;

        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await invoker.ExecuteWriteAsync(
                "host-1",
                isIdempotent: true,
                async _ =>
                {
                    attempts++;
                    await Task.Yield();
                    throw new InvalidOperationException("boom");
#pragma warning disable CS0162
                    return 0;
#pragma warning restore CS0162
                },
                CancellationToken.None));

        attempts.ShouldBe(1, "tier-A default for Write is RetryCount=0");
    }

    /// <summary>Verifies that different hosts are honored independently in the resilience pipeline.</summary>
    [Fact]
    public async Task Execute_HonorsDifferentHosts_Independently()
    {
        var builder = new DriverResiliencePipelineBuilder();
        var invoker = MakeInvoker(builder, new DriverResilienceOptions { Tier = DriverTier.A });

        await invoker.ExecuteAsync(DriverCapability.Read, "host-a", _ => ValueTask.FromResult(1), CancellationToken.None);
        await invoker.ExecuteAsync(DriverCapability.Read, "host-b", _ => ValueTask.FromResult(2), CancellationToken.None);

        builder.CachedPipelineCount.ShouldBe(2);
    }

    /// <summary>
    ///     Core-009 regression: ExecuteWriteAsync's non-idempotent branch must snapshot
    ///     <c>_optionsAccessor</c> exactly once per call. Calling it multiple times allocates
    ///     redundant options objects on the per-write hot path and creates a consistency hazard
    ///     where an Admin edit mid-call could observe two different snapshots.
    /// </summary>
    [Fact]
    public async Task ExecuteWriteAsync_NonIdempotent_Snapshots_Options_Once_Per_Call()
    {
        var options = new DriverResilienceOptions
        {
            Tier = DriverTier.A,
            CapabilityPolicies = new Dictionary<DriverCapability, CapabilityPolicy>
            {
                [DriverCapability.Write] = new(TimeoutSeconds: 2, RetryCount: 3, BreakerFailureThreshold: 5),
            },
        };
        var accessorCalls = 0;
        var invoker = new CapabilityInvoker(
            new DriverResiliencePipelineBuilder(),
            "drv-test",
            () => { Interlocked.Increment(ref accessorCalls); return options; });

        await invoker.ExecuteWriteAsync(
            "host-1",
            isIdempotent: false,
            _ => ValueTask.FromResult(0),
            CancellationToken.None);

        accessorCalls.ShouldBe(1,
            "ExecuteWriteAsync's non-idempotent branch must capture the options snapshot exactly once per call");
    }

    /// <summary>
    ///     S-8 residual: the non-idempotent write arm must record tracker in-flight accounting (start +
    ///     complete) on the caller's host, exactly like the idempotent arms — the previous arm skipped it.
    /// </summary>
    [Fact]
    public async Task NonIdempotentWrite_RecordsTrackerStartAndComplete()
    {
        var tracker = new DriverResilienceStatusTracker();
        var invoker = new CapabilityInvoker(
            new DriverResiliencePipelineBuilder(),
            "drv-test",
            () => new DriverResilienceOptions { Tier = DriverTier.A },
            statusTracker: tracker);

        await invoker.ExecuteWriteAsync(
            "host-1",
            isIdempotent: false,
            _ => ValueTask.FromResult(0),
            CancellationToken.None);

        var snap = tracker.TryGet("drv-test", "host-1");
        snap.ShouldNotBeNull("the non-idempotent write arm must record tracker accounting on the caller's host");
        snap!.CurrentInFlight.ShouldBe(0, "in-flight must return to 0 after the write completes");
    }

    /// <summary>
    ///     01/P-4: the non-idempotent write arm must build the no-retry options snapshot ONCE per invoker
    ///     lifetime (not per call). Two writes to the same host reuse the cached snapshot — the options
    ///     accessor is invoked at most once and only one pipeline is cached.
    /// </summary>
    [Fact]
    public async Task NonIdempotentWrite_ReusesCachedNoRetryOptions()
    {
        var builder = new DriverResiliencePipelineBuilder();
        var accessorCalls = 0;
        var invoker = new CapabilityInvoker(
            builder,
            "drv-test",
            () => { Interlocked.Increment(ref accessorCalls); return new DriverResilienceOptions { Tier = DriverTier.A }; });

        await invoker.ExecuteWriteAsync("host-1", isIdempotent: false, _ => ValueTask.FromResult(0), CancellationToken.None);
        await invoker.ExecuteWriteAsync("host-1", isIdempotent: false, _ => ValueTask.FromResult(0), CancellationToken.None);

        accessorCalls.ShouldBeLessThanOrEqualTo(1, "the no-retry snapshot must be cached across calls");
        builder.CachedPipelineCount.ShouldBe(1, "two writes to the same host share one cached no-retry pipeline");
    }

    /// <summary>
    ///     R2-10: a successful capability call must reset <c>ConsecutiveFailures</c> so the counter
    ///     is a true "consecutive" gauge — nonzero only during an active retry storm, not stuck at the
    ///     last blip's count forever. Previously only the breaker's <c>OnClosed</c> reset it, so a
    ///     transient 2-retry blip that never opened the breaker read "2 failures" indefinitely.
    /// </summary>
    [Fact]
    public async Task ExecuteAsync_OnSuccess_ResetsConsecutiveFailures()
    {
        var tracker = new DriverResilienceStatusTracker();
        tracker.RecordFailure("drv-test", "host-1", DateTime.UtcNow);
        tracker.RecordFailure("drv-test", "host-1", DateTime.UtcNow);
        tracker.TryGet("drv-test", "host-1")!.ConsecutiveFailures.ShouldBe(2, "seeded failures");

        var invoker = new CapabilityInvoker(
            new DriverResiliencePipelineBuilder(),
            "drv-test",
            () => new DriverResilienceOptions { Tier = DriverTier.A },
            statusTracker: tracker);

        await invoker.ExecuteAsync(
            DriverCapability.Read,
            "host-1",
            _ => ValueTask.FromResult(1),
            CancellationToken.None);

        tracker.TryGet("drv-test", "host-1")!.ConsecutiveFailures.ShouldBe(0,
            "a successful capability call must clear the consecutive-failure counter");
    }

    /// <summary>
    ///     R2-10 companion: a successful non-idempotent write must also reset the consecutive-failure
    ///     counter on the caller's host (the write arm has its own success path).
    /// </summary>
    [Fact]
    public async Task ExecuteWriteAsync_NonIdempotent_OnSuccess_ResetsConsecutiveFailures()
    {
        var tracker = new DriverResilienceStatusTracker();
        tracker.RecordFailure("drv-test", "host-1", DateTime.UtcNow);
        tracker.TryGet("drv-test", "host-1")!.ConsecutiveFailures.ShouldBe(1, "seeded failure");

        var invoker = new CapabilityInvoker(
            new DriverResiliencePipelineBuilder(),
            "drv-test",
            () => new DriverResilienceOptions { Tier = DriverTier.A },
            statusTracker: tracker);

        await invoker.ExecuteWriteAsync(
            "host-1",
            isIdempotent: false,
            _ => ValueTask.FromResult(0),
            CancellationToken.None);

        tracker.TryGet("drv-test", "host-1")!.ConsecutiveFailures.ShouldBe(0,
            "a successful non-idempotent write must clear the consecutive-failure counter");
    }

    /// <summary>
    ///     Core-009 regression — companion consistency assertion: the non-idempotent branch must
    ///     not observe two different option snapshots even if the accessor's returned value changes
    ///     between calls (simulating an Admin edit landing mid-flight). With a single snapshot the
    ///     two derived values (<c>with</c> base + <c>Resolve(Write)</c>) come from the same options
    ///     instance.
    /// </summary>
    [Fact]
    public async Task ExecuteWriteAsync_NonIdempotent_Uses_Consistent_Options_Snapshot()
    {
        var a = new DriverResilienceOptions { Tier = DriverTier.A };
        var b = new DriverResilienceOptions { Tier = DriverTier.B };
        var alternating = new[] { a, b, a, b }.AsEnumerable().GetEnumerator();
        var invoker = new CapabilityInvoker(
            new DriverResiliencePipelineBuilder(),
            "drv-test",
            () => { alternating.MoveNext(); return alternating.Current; });

        // If options is read twice, the with-expression and Resolve(Write) come from
        // different tier tables (A then B) — the resulting one-entry dictionary is
        // inconsistent with the snapshot used for the rest of the options. Single-snapshot
        // semantics guarantee the call sees a coherent view.
        await Should.NotThrowAsync(async () => await invoker.ExecuteWriteAsync(
            "host-1",
            isIdempotent: false,
            _ => ValueTask.FromResult(0),
            CancellationToken.None));
    }
}
