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.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 enum DriverCapability { Read, Write, Discover }
}
namespace ZB.MOM.WW.OtOpcUa.Core.Resilience {
    using System;
    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!;
    }
}
""";

    [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");
    }

    [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();
    }

    [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");
    }

    [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");
    }

    [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);
    }

    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();
    }
}
