using Microsoft.Extensions.Logging;
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;

/// <summary>
///     Guards the per-instance ResilienceConfig plumbing (arch-review #13): the factory must layer the
///     per-instance <c>ResilienceConfig</c> JSON onto the tier defaults, invalidate any stale cached
///     pipelines for the instance on every <see cref="DriverCapabilityInvokerFactory.Create"/> (so a
///     respawn with a changed config takes effect despite the options-blind pipeline cache), and log —
///     never throw on — a malformed config.
/// </summary>
[Trait("Category", "Unit")]
public sealed class DriverCapabilityInvokerFactoryTests
{
    private static DriverCapabilityInvokerFactory MakeFactory(
        DriverResiliencePipelineBuilder builder,
        DriverTier tier = DriverTier.A,
        ILogger? logger = null) =>
        new(builder, new DriverResilienceStatusTracker(), _ => tier, logger);

    /// <summary>
    ///     End-to-end proof the per-instance override REACHES execution: a ResilienceConfig that sets
    ///     Read's retryCount to 0 must suppress the tier-A Read retry — a failing Read is attempted once,
    ///     not retried.
    /// </summary>
    [Fact]
    public async Task Create_applies_per_instance_ResilienceConfig_override_to_behaviour()
    {
        var builder = new DriverResiliencePipelineBuilder();
        var factory = MakeFactory(builder);
        // Tier A Read retries by default (see CapabilityInvokerTests.Read_Retries_OnTransientFailure);
        // this override disables it.
        var invoker = factory.Create("d1", "Modbus",
            resilienceConfigJson: "{\"capabilityPolicies\":{\"Read\":{\"retryCount\":0}}}");
        var attempts = 0;

        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await invoker.ExecuteAsync(
                DriverCapability.Read,
                "host-1",
                async _ =>
                {
                    attempts++;
                    await Task.Yield();
                    throw new InvalidOperationException("boom");
                },
                CancellationToken.None));

        attempts.ShouldBe(1, "the per-instance ResilienceConfig override (retryCount:0) must suppress the tier-A Read retry");
    }

    /// <summary>
    ///     A control against the override test: with NO ResilienceConfig the same failing Read retries per
    ///     the tier-A default — confirming the previous test's single attempt is the override, not a fluke.
    /// </summary>
    [Fact]
    public async Task Create_without_ResilienceConfig_uses_tier_default_retry()
    {
        var factory = MakeFactory(new DriverResiliencePipelineBuilder());
        var invoker = factory.Create("d1", "Modbus");
        var attempts = 0;

        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await invoker.ExecuteAsync(
                DriverCapability.Read,
                "host-1",
                async _ =>
                {
                    attempts++;
                    await Task.Yield();
                    throw new InvalidOperationException("boom");
                },
                CancellationToken.None));

        attempts.ShouldBeGreaterThan(1, "tier-A Read retries by default when no override disables it");
    }

    /// <summary>
    ///     Invalidate-on-change: because the pipeline cache is keyed on (instance, host, capability) and
    ///     ignores options on a hit, a fresh <see cref="DriverCapabilityInvokerFactory.Create"/> for an
    ///     instance must drop that instance's cached pipelines so a respawn with new options rebuilds them.
    /// </summary>
    [Fact]
    public async Task Create_invalidates_prior_cached_pipelines_for_the_same_instance()
    {
        var builder = new DriverResiliencePipelineBuilder();
        var factory = MakeFactory(builder);

        // First invoker warms the cache for instance d1.
        var first = factory.Create("d1", "Modbus");
        await first.ExecuteAsync(DriverCapability.Read, "host-1", _ => ValueTask.FromResult(1), CancellationToken.None);
        builder.CachedPipelineCount.ShouldBe(1);

        // Re-creating the invoker for the SAME instance (a respawn) must invalidate the stale pipeline.
        _ = factory.Create("d1", "Modbus", resilienceConfigJson: "{\"bulkheadMaxConcurrent\":8}");

        builder.CachedPipelineCount.ShouldBe(0, "Create must invalidate the instance's cached pipelines so changed options take effect");
    }

    /// <summary>
    ///     Invalidation is scoped to the instance being (re)created — a sibling instance's warm pipeline
    ///     must survive.
    /// </summary>
    [Fact]
    public async Task Create_does_not_invalidate_a_different_instance()
    {
        var builder = new DriverResiliencePipelineBuilder();
        var factory = MakeFactory(builder);

        var other = factory.Create("other", "Modbus");
        await other.ExecuteAsync(DriverCapability.Read, "host-1", _ => ValueTask.FromResult(1), CancellationToken.None);
        builder.CachedPipelineCount.ShouldBe(1);

        _ = factory.Create("d1", "Modbus");

        builder.CachedPipelineCount.ShouldBe(1, "invalidation is per-instance — the sibling's pipeline must remain");
    }

    /// <summary>A malformed ResilienceConfig logs a diagnostic but still yields a working (tier-default) invoker.</summary>
    [Fact]
    public async Task Create_with_malformed_ResilienceConfig_logs_and_falls_back_to_tier_defaults()
    {
        var logger = new CapturingLogger();
        var factory = MakeFactory(new DriverResiliencePipelineBuilder(), logger: logger);

        var invoker = factory.Create("d1", "Modbus", resilienceConfigJson: "{ this is not json ]");

        invoker.ShouldNotBeNull();
        // Still functional: a normal Read succeeds under the fallback tier defaults.
        var result = await invoker.ExecuteAsync(DriverCapability.Read, "host-1", _ => ValueTask.FromResult(7), CancellationToken.None);
        result.ShouldBe(7);
        logger.Entries.ShouldContain(e => e.Level == LogLevel.Warning && e.Message.Contains("resilience config", StringComparison.OrdinalIgnoreCase));
    }

    /// <summary>
    ///     01/S-6 production-wiring proof: the exact <c>DriverHostActor.SpawnChild</c> path
    ///     (<c>factory.Create(id, type, hostileJson)</c>) must yield a WORKING invoker whose capability
    ///     call succeeds, and must log the clamp diagnostic as a warning — a hostile ResilienceConfig can
    ///     never brick the spawn path.
    /// </summary>
    [Fact]
    public async Task Create_with_out_of_range_config_yields_working_invoker()
    {
        var logger = new CapturingLogger();
        var factory = MakeFactory(new DriverResiliencePipelineBuilder(), logger: logger);

        var invoker = factory.Create("i1", "Modbus",
            resilienceConfigJson: "{\"capabilityPolicies\":{\"Subscribe\":{\"timeoutSeconds\":0,\"breakerFailureThreshold\":1}}}");

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

        result.ShouldBe(99, "a hostile ResilienceConfig must not brick the Subscribe capability call");
        logger.Entries.ShouldContain(e =>
            e.Level == LogLevel.Warning &&
            e.Message.Contains("resilience config", StringComparison.OrdinalIgnoreCase) &&
            (e.Message.Contains("timeoutSeconds") || e.Message.Contains("breakerFailureThreshold")));
    }

    /// <summary>
    ///     01/S-7 production-wiring proof: a respawn interleaved with a lingering old-invoker call must still
    ///     apply the new options. The old invoker (a captured reference — exactly what the old child holds)
    ///     re-caches its stale pipeline AFTER the new <see cref="DriverCapabilityInvokerFactory.Create"/>'s
    ///     invalidate; the new invoker must nonetheless behave per the new config. Fails without the per-Create
    ///     generation stamp (both invokers share the (instance, host, capability) key and the stale entry wins).
    /// </summary>
    [Fact]
    public async Task Respawn_interleaved_with_old_invoker_call_still_applies_new_options()
    {
        var builder = new DriverResiliencePipelineBuilder();
        var factory = MakeFactory(builder);

        // Old invoker: Read retries (tier-A default 3). Warm its cache.
        var oldInvoker = factory.Create("d1", "Modbus", "{\"capabilityPolicies\":{\"Read\":{\"retryCount\":3}}}");
        await FailingRead(oldInvoker);   // caches the retrying pipeline under the old generation

        // Respawn with new options: Read no longer retries.
        var newInvoker = factory.Create("d1", "Modbus", "{\"capabilityPolicies\":{\"Read\":{\"retryCount\":0}}}");

        // The lingering old child's late call lands AFTER the new Create's invalidate — re-caching the stale
        // retrying pipeline under the OLD generation.
        await FailingRead(oldInvoker);

        // The new invoker must honor the new options (no retry) — a single attempt.
        var attempts = 0;
        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await newInvoker.ExecuteAsync(
                DriverCapability.Read, "host-1",
                async _ => { attempts++; await Task.Yield(); throw new InvalidOperationException("boom"); },
                CancellationToken.None));

        attempts.ShouldBe(1, "the new invoker must apply the new options (retryCount 0), not the stale re-cache");
    }

    private static async Task FailingRead(IDriverCapabilityInvoker invoker) =>
        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await invoker.ExecuteAsync(
                DriverCapability.Read, "host-1",
                async _ => { await Task.Yield(); throw new InvalidOperationException("boom"); },
                CancellationToken.None));

    private sealed class CapturingLogger : ILogger
    {
        public List<(LogLevel Level, string Message)> Entries { get; } = new();
        public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
        public bool IsEnabled(LogLevel logLevel) => true;
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
            Func<TState, Exception?, string> formatter) =>
            Entries.Add((logLevel, formatter(state, exception)));
    }
}
