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>
///     Exercises the per-call host resolver contract against the shared
///     <see cref="DriverResiliencePipelineBuilder"/> + <see cref="CapabilityInvoker"/> — one
///     dead PLC behind a multi-device driver must NOT open the breaker for healthy sibling
///     PLCs (decision #144).
/// </summary>
[Trait("Category", "Unit")]
public sealed class PerCallHostResolverDispatchTests
{
    private sealed class StaticResolver : IPerCallHostResolver
    {
        private readonly Dictionary<string, string> _map;

        /// <summary>Initializes a new instance of StaticResolver with a predefined mapping.</summary>
        /// <param name="map">The mapping of full references to host names.</param>
        public StaticResolver(Dictionary<string, string> map) => _map = map;

        /// <summary>Resolves a host name from the static mapping.</summary>
        /// <param name="fullReference">The full reference to resolve.</param>
        public string ResolveHost(string fullReference) =>
            _map.TryGetValue(fullReference, out var host) ? host : string.Empty;
    }

    /// <summary>Verifies that a dead PLC does not open the breaker for healthy PLCs when using a per-call resolver.</summary>
    [Fact]
    public async Task DeadPlc_DoesNotOpenBreaker_For_HealthyPlc_With_Resolver()
    {
        // Two PLCs behind one driver. Dead PLC keeps failing; healthy PLC must keep serving.
        var builder = new DriverResiliencePipelineBuilder();
        var options = new DriverResilienceOptions { Tier = DriverTier.B };
        var invoker = new CapabilityInvoker(builder, "drv-modbus", () => options);

        var resolver = new StaticResolver(new Dictionary<string, string>
        {
            ["tag-on-dead"]  = "plc-dead",
            ["tag-on-alive"] = "plc-alive",
        });

        var threshold = options.Resolve(DriverCapability.Read).BreakerFailureThreshold;
        for (var i = 0; i < threshold + 3; i++)
        {
            await Should.ThrowAsync<Exception>(async () =>
                await invoker.ExecuteAsync(
                    DriverCapability.Read,
                    hostName: resolver.ResolveHost("tag-on-dead"),
                    _ => throw new InvalidOperationException("plc-dead unreachable"),
                    CancellationToken.None));
        }

        // Healthy PLC's pipeline is in a different bucket; the first call should succeed
        // without hitting the dead-PLC breaker.
        var aliveAttempts = 0;
        await invoker.ExecuteAsync(
            DriverCapability.Read,
            hostName: resolver.ResolveHost("tag-on-alive"),
            _ => { aliveAttempts++; return ValueTask.FromResult("ok"); },
            CancellationToken.None);

        aliveAttempts.ShouldBe(1, "decision #144 — per-PLC isolation keeps healthy PLCs serving");
    }

    /// <summary>Verifies that empty string from resolver is treated as single-host fallback.</summary>
    [Fact]
    public void Resolver_EmptyString_Treated_As_Single_Host_Fallback()
    {
        var resolver = new StaticResolver(new Dictionary<string, string>
        {
            ["tag-unknown"] = "",
        });

        resolver.ResolveHost("tag-unknown").ShouldBe("");
        resolver.ResolveHost("not-in-map").ShouldBe("", "unknown refs return empty so dispatch falls back to single-host");
    }

    /// <summary>Verifies that without a resolver, the same host shares one resilience pipeline.</summary>
    [Fact]
    public async Task WithoutResolver_SameHost_Shares_One_Pipeline()
    {
        // Without a resolver all calls share the DriverInstanceId pipeline — that's the
        // pre-decision-#144 behavior single-host drivers should keep.
        var builder = new DriverResiliencePipelineBuilder();
        var options = new DriverResilienceOptions { Tier = DriverTier.A };
        var invoker = new CapabilityInvoker(builder, "drv-single", () => options);

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

        builder.CachedPipelineCount.ShouldBe(1, "single-host drivers share one pipeline");
    }

    /// <summary>Verifies that with a resolver, different hosts get separate resilience pipelines.</summary>
    [Fact]
    public async Task WithResolver_TwoHosts_Get_Two_Pipelines()
    {
        var builder = new DriverResiliencePipelineBuilder();
        var options = new DriverResilienceOptions { Tier = DriverTier.B };
        var invoker = new CapabilityInvoker(builder, "drv-modbus", () => options);
        var resolver = new StaticResolver(new Dictionary<string, string>
        {
            ["tag-a"] = "plc-a",
            ["tag-b"] = "plc-b",
        });

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

        builder.CachedPipelineCount.ShouldBe(2, "each host keyed on its own pipeline");
    }
}
