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 AlarmSurfaceInvokerTests
{
    private static readonly DriverResilienceOptions TierAOptions = new() { Tier = DriverTier.A };

    [Fact]
    public async Task SubscribeAsync_EmptyList_ReturnsEmpty_WithoutDriverCall()
    {
        var driver = new FakeAlarmSource();
        var surface = NewSurface(driver, defaultHost: "h");

        var handles = await surface.SubscribeAsync([], CancellationToken.None);

        handles.Count.ShouldBe(0);
        driver.SubscribeCallCount.ShouldBe(0);
    }

    [Fact]
    public async Task SubscribeAsync_SingleHost_RoutesThroughDefaultHost()
    {
        var driver = new FakeAlarmSource();
        var surface = NewSurface(driver, defaultHost: "h1");

        var handles = await surface.SubscribeAsync(["src-1", "src-2"], CancellationToken.None);

        handles.Count.ShouldBe(1);
        driver.SubscribeCallCount.ShouldBe(1);
        driver.LastSubscribedIds.ShouldBe(["src-1", "src-2"]);
    }

    [Fact]
    public async Task SubscribeAsync_MultiHost_FansOutByResolvedHost()
    {
        var driver = new FakeAlarmSource();
        var resolver = new StubResolver(new Dictionary<string, string>
        {
            ["src-1"] = "plc-a",
            ["src-2"] = "plc-b",
            ["src-3"] = "plc-a",
        });
        var surface = NewSurface(driver, defaultHost: "default-ignored", resolver: resolver);

        var handles = await surface.SubscribeAsync(["src-1", "src-2", "src-3"], CancellationToken.None);

        handles.Count.ShouldBe(2);                 // one per distinct host
        driver.SubscribeCallCount.ShouldBe(2);     // one driver call per host
    }

    [Fact]
    public async Task AcknowledgeAsync_DoesNotRetry_OnFailure()
    {
        var driver = new FakeAlarmSource { AcknowledgeShouldThrow = true };
        var surface = NewSurface(driver, defaultHost: "h1");

        await Should.ThrowAsync<InvalidOperationException>(() =>
            surface.AcknowledgeAsync([new AlarmAcknowledgeRequest("s", "c", null)], CancellationToken.None));

        driver.AcknowledgeCallCount.ShouldBe(1, "AlarmAcknowledge must not retry — decision #143");
    }

    [Fact]
    public async Task SubscribeAsync_Retries_Transient_Failures()
    {
        var driver = new FakeAlarmSource { SubscribeFailuresBeforeSuccess = 2 };
        var surface = NewSurface(driver, defaultHost: "h1");

        await surface.SubscribeAsync(["src"], CancellationToken.None);

        driver.SubscribeCallCount.ShouldBe(3, "AlarmSubscribe retries by default — decision #143");
    }

    private static AlarmSurfaceInvoker NewSurface(
        IAlarmSource driver,
        string defaultHost,
        IPerCallHostResolver? resolver = null)
    {
        var builder = new DriverResiliencePipelineBuilder();
        var invoker = new CapabilityInvoker(builder, "drv-1", () => TierAOptions);
        return new AlarmSurfaceInvoker(invoker, driver, defaultHost, resolver);
    }

    private sealed class FakeAlarmSource : IAlarmSource
    {
        public int SubscribeCallCount { get; private set; }
        public int AcknowledgeCallCount { get; private set; }
        public int SubscribeFailuresBeforeSuccess { get; set; }
        public bool AcknowledgeShouldThrow { get; set; }
        public IReadOnlyList<string> LastSubscribedIds { get; private set; } = [];

        public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
            IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
        {
            SubscribeCallCount++;
            LastSubscribedIds = sourceNodeIds;
            if (SubscribeCallCount <= SubscribeFailuresBeforeSuccess)
                throw new InvalidOperationException("transient");
            return Task.FromResult<IAlarmSubscriptionHandle>(new StubHandle($"h-{SubscribeCallCount}"));
        }

        public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
            => Task.CompletedTask;

        public Task AcknowledgeAsync(
            IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
        {
            AcknowledgeCallCount++;
            if (AcknowledgeShouldThrow) throw new InvalidOperationException("ack boom");
            return Task.CompletedTask;
        }

        public event EventHandler<AlarmEventArgs>? OnAlarmEvent { add { } remove { } }
    }

    private sealed record StubHandle(string DiagnosticId) : IAlarmSubscriptionHandle;

    private sealed class StubResolver(Dictionary<string, string> map) : IPerCallHostResolver
    {
        public string ResolveHost(string fullReference) => map[fullReference];
    }
}
