using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Analyzers;

namespace ZB.MOM.WW.OtOpcUa.Analyzers.Tests;

/// <summary>
///     Compile-a-snippet-and-run-the-analyzer tests. Avoids
///     Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit because it pins to xunit v2 +
///     this project uses xunit.v3 like the rest of the solution. Hand-rolled harness is 15
///     lines + makes the assertion surface obvious at the test-author level.
/// </summary>
[Trait("Category", "Unit")]
public sealed class UnwrappedCapabilityCallAnalyzerTests
{
    /// <summary>Minimal stubs for the guarded interfaces + the two wrapper types. Keeps the
    /// analyzer tests independent of the real OtOpcUa project references so a drift in those
    /// signatures doesn't secretly mute the analyzer check.</summary>
    private const string StubSources = """
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions {
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Collections.Generic;
    public interface IReadable {
        ValueTask<IReadOnlyList<object>> ReadAsync(IReadOnlyList<string> tags, CancellationToken ct);
    }
    public interface IWritable {
        ValueTask WriteAsync(IReadOnlyList<object> ops, CancellationToken ct);
    }
    public interface ITagDiscovery {
        Task DiscoverAsync(CancellationToken ct);
    }
    public interface ISubscriptionHandle { string DiagnosticId { get; } }
    public interface ISubscribable {
        Task<ISubscriptionHandle> SubscribeAsync(IReadOnlyList<string> refs, TimeSpan interval, CancellationToken ct);
        Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken ct);
    }
    public interface IAlarmSubscriptionHandle { string DiagnosticId { get; } }
    public interface IAlarmSource {
        Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(IReadOnlyList<string> sourceNodeIds, CancellationToken ct);
        Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken ct);
        Task AcknowledgeAsync(IReadOnlyList<string> acks, CancellationToken ct);
    }
    public class HostConnectivityStatus { public string HostName { get; init; } = ""; }
    public interface IHostConnectivityProbe {
        IReadOnlyList<HostConnectivityStatus> GetHostStatuses();
    }
    public class HistoryReadResult { }
    public class HistoricalEventsResult { }
    public interface IHistoryProvider {
        Task<HistoryReadResult> ReadRawAsync(string fullRef, DateTime start, DateTime end, uint max, CancellationToken ct);
        Task<HistoryReadResult> ReadProcessedAsync(string fullRef, DateTime start, DateTime end, TimeSpan interval, CancellationToken ct);
        Task<HistoryReadResult> ReadAtTimeAsync(string fullRef, IReadOnlyList<DateTime> timestamps, CancellationToken ct)
            => throw new NotSupportedException();
        Task<HistoricalEventsResult> ReadEventsAsync(string? sourceName, DateTime start, DateTime end, int maxEvents, CancellationToken ct)
            => throw new NotSupportedException();
    }
    public enum DriverCapability { Read, Write, Discover, AlarmSubscribe, AlarmAcknowledge }
    public interface IDriverCapabilityInvoker {
        ValueTask<T> ExecuteAsync<T>(DriverCapability c, string host, Func<CancellationToken, ValueTask<T>> call, CancellationToken ct);
        ValueTask ExecuteAsync(DriverCapability c, string host, Func<CancellationToken, ValueTask> call, CancellationToken ct);
        ValueTask<T> ExecuteWriteAsync<T>(string host, bool isIdempotent, Func<CancellationToken, ValueTask<T>> call, CancellationToken ct);
    }
}
namespace ZB.MOM.WW.OtOpcUa.Core.Resilience {
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
    public sealed class CapabilityInvoker {
        public ValueTask<T> ExecuteAsync<T>(DriverCapability c, string host, Func<CancellationToken, ValueTask<T>> call, CancellationToken ct) => throw null!;
        public ValueTask ExecuteAsync(DriverCapability c, string host, Func<CancellationToken, ValueTask> call, CancellationToken ct) => throw null!;
        public ValueTask<T> ExecuteWriteAsync<T>(string host, bool isIdempotent, Func<CancellationToken, ValueTask<T>> call, CancellationToken ct) => throw null!;
        public ValueTask ExecuteWriteAsync(string host, bool isIdempotent, Func<CancellationToken, ValueTask> call, CancellationToken ct) => throw null!;
    }
    public sealed class AlarmSurfaceInvoker {
        public Task<IReadOnlyList<IAlarmSubscriptionHandle>> SubscribeAsync(IReadOnlyList<string> sourceNodeIds, CancellationToken ct) => throw null!;
        public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken ct) => throw null!;
        public Task AcknowledgeAsync(IReadOnlyList<string> acks, CancellationToken ct) => throw null!;
    }
}
""";

    /// <summary>Verifies that a direct ReadAsync call in the Server namespace trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_ReadAsync_Call_InServerNamespace_TripsDiagnostic()
    {
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadCaller {
        public async Task DoIt(IReadable driver) {
            var _ = await driver.ReadAsync(new List<string>(), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].Id.ShouldBe(UnwrappedCapabilityCallAnalyzer.DiagnosticId);
        diags[0].GetMessage().ShouldContain("ReadAsync");
    }

    /// <summary>Verifies that a wrapped ReadAsync call inside a CapabilityInvoker lambda passes cleanly.</summary>
    [Fact]
    public async Task Wrapped_ReadAsync_InsideCapabilityInvokerLambda_PassesCleanly()
    {
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodCaller {
        public async Task DoIt(IReadable driver, CapabilityInvoker invoker) {
            var _ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
                async ct => await driver.ReadAsync(new List<string>(), ct), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    /// <summary>The Polly-free Runtime dispatch layer wraps calls through the
    /// <c>IDriverCapabilityInvoker</c> seam (it cannot reference the concrete <c>CapabilityInvoker</c>).
    /// An interface-typed invoker call must be recognised as a wrapper home too — arch-review #10.</summary>
    [Fact]
    public async Task Wrapped_InsideDriverCapabilityInvokerInterfaceLambda_PassesCleanly()
    {
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodSeamCaller {
        public async Task Sub(ISubscribable driver, IDriverCapabilityInvoker invoker) {
            var _ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
                async ct => await driver.SubscribeAsync(new List<string>(), System.TimeSpan.Zero, ct), CancellationToken.None);
        }
        public async Task Wr(IReadable driver, IDriverCapabilityInvoker invoker) {
            // ExecuteWriteAsync interface overload is a wrapper home too (value-returning call wrapped).
            var _ = await invoker.ExecuteWriteAsync("h1", true,
                async ct => await driver.ReadAsync(new List<string>(), ct), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    /// <summary>Verifies that a direct write without wrapper trips the diagnostic.</summary>
    [Fact]
    public async Task DirectWrite_WithoutWrapper_TripsDiagnostic()
    {
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadWrite {
        public async Task DoIt(IWritable driver) {
            await driver.WriteAsync(new List<object>(), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("WriteAsync");
    }

    /// <summary>Verifies that a discovery call without wrapper trips the diagnostic.</summary>
    [Fact]
    public async Task Discovery_Call_WithoutWrapper_TripsDiagnostic()
    {
        const string userSrc = """
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadDiscover {
        public async Task DoIt(ITagDiscovery driver) {
            await driver.DiscoverAsync(CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("DiscoverAsync");
    }

    /// <summary>Verifies that a call outside of a lambda but inside an invoker call still trips the diagnostic.</summary>
    [Fact]
    public async Task Call_OutsideOfLambda_ButInsideInvokerCall_StillTripsDiagnostic()
    {
        // Precompute the read *outside* the lambda, then pass the awaited result — that does NOT
        // actually wrap the ReadAsync call in the resilience pipeline, so the analyzer must
        // still flag it (regression guard: a naive "any mention of ExecuteAsync nearby" rule
        // would silently let this pattern through).
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class SneakyCaller {
        public async Task DoIt(IReadable driver, CapabilityInvoker invoker) {
            var result = await driver.ReadAsync(new List<string>(), CancellationToken.None); // not inside any lambda
            await invoker.ExecuteAsync(DriverCapability.Read, "h1",
                async ct => { await Task.Yield(); }, CancellationToken.None);
            _ = result;
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
    }

    // -----------------------------------------------------------------------
    // ISubscribable
    // -----------------------------------------------------------------------

    /// <summary>Verifies that a direct SubscribeAsync call trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_SubscribeAsync_Call_TripsDiagnostic()
    {
        const string userSrc = """
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadSubscribe {
        public async Task DoIt(ISubscribable driver) {
            _ = await driver.SubscribeAsync(new List<string>(), TimeSpan.FromSeconds(1), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("SubscribeAsync");
    }

    /// <summary>Verifies that a wrapped SubscribeAsync call inside a CapabilityInvoker lambda passes cleanly.</summary>
    [Fact]
    public async Task Wrapped_SubscribeAsync_InsideCapabilityInvokerLambda_PassesCleanly()
    {
        const string userSrc = """
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodSubscribe {
        public async Task DoIt(ISubscribable driver, CapabilityInvoker invoker) {
            _ = await invoker.ExecuteAsync(DriverCapability.AlarmSubscribe, "h1",
                async ct => await driver.SubscribeAsync(new List<string>(), TimeSpan.FromSeconds(1), ct),
                CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // -----------------------------------------------------------------------
    // IAlarmSource
    // -----------------------------------------------------------------------

    /// <summary>Verifies that a direct SubscribeAlarmsAsync call trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_SubscribeAlarmsAsync_Call_TripsDiagnostic()
    {
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadAlarmSubscribe {
        public async Task DoIt(IAlarmSource source) {
            _ = await source.SubscribeAlarmsAsync(new List<string>(), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("SubscribeAlarmsAsync");
    }

    /// <summary>Verifies that a wrapped SubscribeAlarmsAsync call inside a CapabilityInvoker lambda passes cleanly.</summary>
    [Fact]
    public async Task Wrapped_SubscribeAlarmsAsync_InsideCapabilityInvokerLambda_PassesCleanly()
    {
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodAlarmSubscribe {
        public async Task DoIt(IAlarmSource source, CapabilityInvoker invoker) {
            _ = await invoker.ExecuteAsync(DriverCapability.AlarmSubscribe, "h1",
                async ct => await source.SubscribeAlarmsAsync(new List<string>(), ct),
                CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // -----------------------------------------------------------------------
    // IHistoryProvider — interface-typed receiver
    // -----------------------------------------------------------------------

    /// <summary>Verifies that a direct ReadRawAsync call on an interface trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_ReadRawAsync_OnInterface_TripsDiagnostic()
    {
        const string userSrc = """
using System;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadHistoryRead {
        public async Task DoIt(IHistoryProvider provider) {
            _ = await provider.ReadRawAsync("tag", DateTime.MinValue, DateTime.MaxValue, 100, CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadRawAsync");
    }

    /// <summary>Verifies that a direct ReadProcessedAsync call on an interface trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_ReadProcessedAsync_OnInterface_TripsDiagnostic()
    {
        const string userSrc = """
using System;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadHistoryProcessed {
        public async Task DoIt(IHistoryProvider provider) {
            _ = await provider.ReadProcessedAsync("tag", DateTime.MinValue, DateTime.MaxValue, TimeSpan.FromMinutes(1), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadProcessedAsync");
    }

    /// <summary>Verifies that a direct ReadAtTimeAsync call on an interface trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_ReadAtTimeAsync_OnInterface_TripsDiagnostic()
    {
        // ReadAtTimeAsync is a default interface method — the analyzer must still flag it when
        // called through an interface-typed receiver.
        const string userSrc = """
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadHistoryAtTime {
        public async Task DoIt(IHistoryProvider provider) {
            _ = await provider.ReadAtTimeAsync("tag", new List<DateTime>(), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadAtTimeAsync");
    }

    /// <summary>Verifies that a wrapped ReadRawAsync call inside a CapabilityInvoker lambda passes cleanly.</summary>
    [Fact]
    public async Task Wrapped_ReadRawAsync_InsideCapabilityInvokerLambda_PassesCleanly()
    {
        const string userSrc = """
using System;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodHistoryRead {
        public async Task DoIt(IHistoryProvider provider, CapabilityInvoker invoker) {
            _ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
                async ct => await provider.ReadRawAsync("tag", DateTime.MinValue, DateTime.MaxValue, 100, ct),
                CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // -----------------------------------------------------------------------
    // IHistoryProvider — concrete driver receiver (FindImplementationForInterfaceMember branch)
    // -----------------------------------------------------------------------

    /// <summary>Verifies that a direct HistoryRead call on a concrete driver trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_HistoryRead_OnConcreteDriver_TripsDiagnostic()
    {
        // ConcreteHistoryDriver implements IHistoryProvider with explicitly-named methods
        // (same names here, but the test exercises the FindImplementationForInterfaceMember
        // branch because the receiver type is the concrete class, not the interface).
        const string userSrc = """
using System;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class ConcreteHistoryDriver : IHistoryProvider {
        public Task<HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => throw null!;
        public Task<HistoryReadResult> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, CancellationToken ct) => throw null!;
    }
    public sealed class BadConcreteHistoryRead {
        public async Task DoIt(ConcreteHistoryDriver driver) {
            _ = await driver.ReadRawAsync("tag", DateTime.MinValue, DateTime.MaxValue, 100, CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadRawAsync");
    }

    // -----------------------------------------------------------------------
    // IHostConnectivityProbe — synchronous member must NOT be flagged
    // -----------------------------------------------------------------------

    /// <summary>Verifies that a synchronous GetHostStatuses call is not flagged.</summary>
    [Fact]
    public async Task Synchronous_GetHostStatuses_IsNotFlagged()
    {
        // GetHostStatuses() is synchronous — the IsAsyncReturningType filter must exclude it.
        const string userSrc = """
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class SafeConnectivityCaller {
        public void DoIt(IHostConnectivityProbe probe) {
            _ = probe.GetHostStatuses();
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // -----------------------------------------------------------------------
    // Concrete driver receiver with a renamed implementing method
    // -----------------------------------------------------------------------

    /// <summary>Verifies that a concrete driver with a renamed read method still trips the diagnostic.</summary>
    [Fact]
    public async Task ConcreteDriver_RenamedReadMethod_StillTripsDiagnostic()
    {
        // A driver whose read implementation has a different name than ReadAsync but is the
        // explicit interface implementation — the FindImplementationForInterfaceMember branch
        // in ImplementsGuardedInterface must catch it.
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Drivers.Fancy {
    public sealed class FancyDriver : IReadable {
        ValueTask<IReadOnlyList<object>> IReadable.ReadAsync(IReadOnlyList<string> tags, CancellationToken ct)
            => InternalFetchAsync(tags, ct);
        public ValueTask<IReadOnlyList<object>> InternalFetchAsync(IReadOnlyList<string> tags, CancellationToken ct)
            => throw null!;
    }
}
namespace ZB.MOM.WW.OtOpcUa.Server {
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
    using ZB.MOM.WW.OtOpcUa.Drivers.Fancy;
    public sealed class BadFancyCaller {
        public async Task DoIt(FancyDriver driver) {
            // Called through the interface — explicit implementation routes to IReadable.ReadAsync
            var iface = (IReadable)driver;
            var _ = await iface.ReadAsync(new List<string>(), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadAsync");
    }

    // -----------------------------------------------------------------------
    // ExecuteWriteAsync wrapping
    // -----------------------------------------------------------------------

    /// <summary>Verifies that a wrapped WriteAsync call inside ExecuteWriteAsync passes cleanly.</summary>
    [Fact]
    public async Task Wrapped_WriteAsync_InsideExecuteWriteAsync_PassesCleanly()
    {
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodWriteViaExecuteWrite {
        public async Task DoIt(IWritable driver, CapabilityInvoker invoker) {
            await invoker.ExecuteWriteAsync("h1", isIdempotent: false,
                async ct => await driver.WriteAsync(new List<object>(), ct),
                CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // -----------------------------------------------------------------------
    // Nested lambda — inner guarded call must not be suppressed by outer non-wrapper lambda
    // -----------------------------------------------------------------------

    /// <summary>Verifies that a guarded call inside a nested non-wrapper lambda trips the diagnostic.</summary>
    [Fact]
    public async Task GuardedCall_InsideNestedNonWrapperLambda_TripsDiagnostic()
    {
        // The ReadAsync call is inside a Func<...> that is NOT an argument to a wrapper method.
        // A naive ancestry walk that stops at any lambda would silently suppress this.
        const string userSrc = """
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class NestedLambdaCaller {
        public async Task DoIt(IReadable driver) {
            Func<Task> work = async () => {
                _ = await driver.ReadAsync(new List<string>(), CancellationToken.None);
            };
            await work();
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadAsync");
    }

    /// <summary>Verifies that a guarded call inside a wrapper lambda inside a non-wrapper lambda passes cleanly.</summary>
    [Fact]
    public async Task GuardedCall_InsideWrapperLambda_InsideNonWrapperLambda_PassesCleanly()
    {
        // The ReadAsync call IS inside a CapabilityInvoker.ExecuteAsync lambda,
        // even though that lambda is also nested inside an outer non-wrapper lambda.
        const string userSrc = """
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class DoubleNestedGoodCaller {
        public async Task DoIt(IReadable driver, CapabilityInvoker invoker) {
            Func<Task> work = async () => {
                _ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
                    async ct => await driver.ReadAsync(new List<string>(), ct),
                    CancellationToken.None);
            };
            await work();
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // =======================================================================
    // Analyzers-002 — AlarmSurfaceInvoker has no lambda parameters; calls inside
    // its own method bodies are covered transitively by the CapabilityInvoker
    // match because the actual wrapping lambda lives on _invoker.ExecuteAsync.
    // =======================================================================

    /// <summary>Verifies that a guarded call inside an alarm surface invoker method wrapped by CapabilityInvoker passes cleanly.</summary>
    [Fact]
    public async Task GuardedCall_InsideAlarmSurfaceInvokerMethod_WrappedByCapabilityInvoker_PassesCleanly()
    {
        // Mirrors the real AlarmSurfaceInvoker pattern: it owns an inner CapabilityInvoker
        // and routes IAlarmSource calls through CapabilityInvoker.ExecuteAsync. The transitive
        // CapabilityInvoker wrapper match is what suppresses the diagnostic — the analyzer
        // does NOT need an AlarmSurfaceInvoker-typed lambda escape hatch (it has no lambda
        // params anyway).
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Core.Resilience.Test {
    public sealed class FakeAlarmSurface {
        private readonly CapabilityInvoker _invoker;
        private readonly IAlarmSource _source;
        public FakeAlarmSurface(CapabilityInvoker i, IAlarmSource s) { _invoker = i; _source = s; }
        public async Task<IAlarmSubscriptionHandle> SubscribeAsync(IReadOnlyList<string> ids, CancellationToken ct) {
            return await _invoker.ExecuteAsync(DriverCapability.AlarmSubscribe, "h1",
                async cct => await _source.SubscribeAlarmsAsync(ids, cct), ct);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // =======================================================================
    // Analyzers-003 — false positives must not appear on valid code. We can't
    // easily force a null SemanticModel through RegisterOperationAction, but we
    // can pin the no-false-positive contract for unrelated invocations.
    // =======================================================================

    /// <summary>Verifies that a non-guarded async call does not trip the diagnostic.</summary>
    [Fact]
    public async Task NonGuardedAsyncCall_DoesNotTrip()
    {
        // Unrelated async call on a non-guarded type — must not be flagged regardless of
        // analyzer internals (semantic-model availability, etc.).
        const string userSrc = """
using System.Threading;
using System.Threading.Tasks;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public interface IUnrelated { Task DoStuffAsync(CancellationToken ct); }
    public sealed class UnrelatedCaller {
        public async Task DoIt(IUnrelated x) {
            await x.DoStuffAsync(CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // =======================================================================
    // Analyzers-004 — when the Core.Abstractions guarded interfaces are not
    // referenced at all, the analyzer must be a no-op for the compilation.
    // =======================================================================

    /// <summary>Verifies that a compilation without guarded interface references emits no diagnostics.</summary>
    [Fact]
    public async Task Compilation_WithoutGuardedInterfaceReferences_EmitsNoDiagnostics()
    {
        // A standalone compilation that does not pull in the StubSources at all — the
        // RegisterCompilationStartAction symbol-resolution fast-path must skip cleanly
        // when none of the guarded types exist.
        const string userSrc = """
using System.Threading;
using System.Threading.Tasks;

namespace SomeOther {
    public interface IReadable { Task ReadAsync(CancellationToken ct); }
    public sealed class Caller {
        public async Task DoIt(IReadable x) {
            await x.ReadAsync(CancellationToken.None);
        }
    }
}
""";
        var diags = await CompileWithoutStubs(userSrc);
        diags.ShouldBeEmpty();
    }

    // =======================================================================
    // Analyzers-005 — IHistoryProvider default-interface-method asymmetry
    // =======================================================================

    /// <summary>Verifies that a direct ReadAtTimeAsync call on a concrete driver inheriting the DIM trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_ReadAtTimeAsync_OnConcreteDriverInheritingDIM_TripsDiagnostic()
    {
        // ConcreteHistoryDriver does NOT override ReadAtTimeAsync (it inherits the DIM).
        // The call site has a concrete-receiver type — the analyzer must still flag the call
        // because the bound method (ReadAtTimeAsync) is the interface's own default impl.
        const string userSrc = """
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class PartialHistoryDriver : IHistoryProvider {
        public Task<HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => throw null!;
        public Task<HistoryReadResult> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, CancellationToken ct) => throw null!;
        // ReadAtTimeAsync + ReadEventsAsync NOT overridden — driver inherits the DIM throws.
    }
    public sealed class BadPartialHistoryCaller {
        public async Task DoIt(PartialHistoryDriver driver) {
            // Cast through the interface so the binder resolves to IHistoryProvider.ReadAtTimeAsync
            // (the DIM). FindImplementationForInterfaceMember returns the interface method itself
            // when nothing overrides it.
            var iface = (IHistoryProvider)driver;
            _ = await iface.ReadAtTimeAsync("tag", new List<DateTime>(), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadAtTimeAsync");
    }

    /// <summary>Verifies that a direct ReadEventsAsync call on a concrete driver inheriting the DIM trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_ReadEventsAsync_OnConcreteDriverInheritingDIM_TripsDiagnostic()
    {
        // Same as above but for ReadEventsAsync — confirms both DIMs are caught when the
        // driver doesn't override them.
        const string userSrc = """
using System;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class PartialHistoryDriver2 : IHistoryProvider {
        public Task<HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => throw null!;
        public Task<HistoryReadResult> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, CancellationToken ct) => throw null!;
    }
    public sealed class BadPartialHistoryEventsCaller {
        public async Task DoIt(PartialHistoryDriver2 driver) {
            var iface = (IHistoryProvider)driver;
            _ = await iface.ReadEventsAsync(null, DateTime.MinValue, DateTime.MaxValue, 100, CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadEventsAsync");
    }

    /// <summary>Verifies that a direct ReadAtTimeAsync call on a concrete driver overriding the DIM trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_ReadAtTimeAsync_OnConcreteDriverOverridingDIM_TripsDiagnostic()
    {
        // Driver explicitly overrides ReadAtTimeAsync — the concrete-receiver call goes through
        // the override; FindImplementationForInterfaceMember returns the override method, which
        // SymbolEqualityComparer matches against method.
        const string userSrc = """
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class FullHistoryDriver : IHistoryProvider {
        public Task<HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => throw null!;
        public Task<HistoryReadResult> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, CancellationToken ct) => throw null!;
        public Task<HistoryReadResult> ReadAtTimeAsync(string r, IReadOnlyList<DateTime> ts, CancellationToken ct) => throw null!;
    }
    public sealed class BadFullHistoryCaller {
        public async Task DoIt(FullHistoryDriver driver) {
            _ = await driver.ReadAtTimeAsync("tag", new List<DateTime>(), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("ReadAtTimeAsync");
    }

    /// <summary>Verifies that a wrapped ReadAtTimeAsync DIM call inside a CapabilityInvoker lambda passes cleanly.</summary>
    [Fact]
    public async Task Wrapped_ReadAtTimeAsync_DIM_InsideCapabilityInvokerLambda_PassesCleanly()
    {
        // DIM wrapped properly — must not trip.
        const string userSrc = """
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodHistoryAtTime {
        public async Task DoIt(IHistoryProvider provider, CapabilityInvoker invoker) {
            _ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
                async ct => await provider.ReadAtTimeAsync("tag", new List<DateTime>(), ct),
                CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    // =======================================================================
    // Analyzers-008 — missing trip + pass-clean tests for ISubscribable.UnsubscribeAsync,
    // IAlarmSource.UnsubscribeAlarmsAsync, IAlarmSource.AcknowledgeAsync, and the wrapped
    // (pass-clean) path for IHistoryProvider.ReadEventsAsync (DIM).
    // =======================================================================

    /// <summary>Verifies that a direct UnsubscribeAsync call trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_UnsubscribeAsync_Call_TripsDiagnostic()
    {
        const string userSrc = """
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadUnsubscribe {
        public async Task DoIt(ISubscribable driver, ISubscriptionHandle handle) {
            await driver.UnsubscribeAsync(handle, CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("UnsubscribeAsync");
    }

    /// <summary>Verifies that a wrapped UnsubscribeAsync call inside a CapabilityInvoker lambda passes cleanly.</summary>
    [Fact]
    public async Task Wrapped_UnsubscribeAsync_InsideCapabilityInvokerLambda_PassesCleanly()
    {
        const string userSrc = """
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodUnsubscribe {
        public async Task DoIt(ISubscribable driver, ISubscriptionHandle handle, CapabilityInvoker invoker) {
            await invoker.ExecuteAsync(DriverCapability.AlarmSubscribe, "h1",
                async ct => await driver.UnsubscribeAsync(handle, ct),
                CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    /// <summary>Verifies that a direct UnsubscribeAlarmsAsync call trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_UnsubscribeAlarmsAsync_Call_TripsDiagnostic()
    {
        const string userSrc = """
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadAlarmUnsubscribe {
        public async Task DoIt(IAlarmSource source, IAlarmSubscriptionHandle handle) {
            await source.UnsubscribeAlarmsAsync(handle, CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("UnsubscribeAlarmsAsync");
    }

    /// <summary>Verifies that a direct AcknowledgeAsync call trips the diagnostic.</summary>
    [Fact]
    public async Task Direct_AcknowledgeAsync_Call_TripsDiagnostic()
    {
        const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class BadAcknowledge {
        public async Task DoIt(IAlarmSource source) {
            await source.AcknowledgeAsync(new List<string>(), CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.Length.ShouldBe(1);
        diags[0].GetMessage().ShouldContain("AcknowledgeAsync");
    }

    /// <summary>Verifies that a wrapped ReadEventsAsync DIM call inside a CapabilityInvoker lambda passes cleanly.</summary>
    [Fact]
    public async Task Wrapped_ReadEventsAsync_DIM_InsideCapabilityInvokerLambda_PassesCleanly()
    {
        // ReadEventsAsync is a DIM — the wrapped path must not trip the diagnostic.
        const string userSrc = """
using System;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;

namespace ZB.MOM.WW.OtOpcUa.Server {
    public sealed class GoodHistoryEvents {
        public async Task DoIt(IHistoryProvider provider, CapabilityInvoker invoker) {
            _ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
                async ct => await provider.ReadEventsAsync(null, DateTime.MinValue, DateTime.MaxValue, 0, ct),
                CancellationToken.None);
        }
    }
}
""";
        var diags = await Compile(userSrc);
        diags.ShouldBeEmpty();
    }

    private static async Task<ImmutableArray<Diagnostic>> CompileWithoutStubs(string userSource)
    {
        var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(userSource) };
        var references = AppDomain.CurrentDomain.GetAssemblies()
            .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
            .Select(a => MetadataReference.CreateFromFile(a.Location))
            .Cast<MetadataReference>()
            .ToList();

        var compilation = CSharpCompilation.Create(
            assemblyName: "AnalyzerTestAssembly_NoStubs",
            syntaxTrees: syntaxTrees,
            references: references,
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        var withAnalyzers = compilation.WithAnalyzers(
            ImmutableArray.Create<DiagnosticAnalyzer>(new UnwrappedCapabilityCallAnalyzer()));

        var allDiags = await withAnalyzers.GetAnalyzerDiagnosticsAsync();
        return allDiags.Where(d => d.Id == UnwrappedCapabilityCallAnalyzer.DiagnosticId).ToImmutableArray();
    }

    private static async Task<ImmutableArray<Diagnostic>> Compile(string userSource)
    {
        var syntaxTrees = new[]
        {
            CSharpSyntaxTree.ParseText(StubSources),
            CSharpSyntaxTree.ParseText(userSource),
        };
        var references = AppDomain.CurrentDomain.GetAssemblies()
            .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
            .Select(a => MetadataReference.CreateFromFile(a.Location))
            .Cast<MetadataReference>()
            .ToList();

        var compilation = CSharpCompilation.Create(
            assemblyName: "AnalyzerTestAssembly",
            syntaxTrees: syntaxTrees,
            references: references,
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        var withAnalyzers = compilation.WithAnalyzers(
            ImmutableArray.Create<DiagnosticAnalyzer>(new UnwrappedCapabilityCallAnalyzer()));

        var allDiags = await withAnalyzers.GetAnalyzerDiagnosticsAsync();
        return allDiags.Where(d => d.Id == UnwrappedCapabilityCallAnalyzer.DiagnosticId).ToImmutableArray();
    }
}
