// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

/*
 * This class contains the logic for suppressing diagnostics that are
 * emitted by the linker analyzers when encountering the framework-provided
 * `Map` invocations. Pending the completion of https://github.com/dotnet/roslyn/issues/68669,
 * this workaround is necessary to apply these suppressions for `Map` invocations that the RDG
 * is able to generate code at compile time for that the analyzer is not able to resolve.
 */

namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class RequestDelegateGeneratorSuppressor : DiagnosticSuppressor
{
    private static readonly SuppressionDescriptor SuppressRUCDiagnostic = new(
        id: "RDGS001",
        suppressedDiagnosticId: "IL2026",
        justification: "The target method has been intercepted by a statically generated variant.");

    private static readonly SuppressionDescriptor SuppressRDCDiagnostic = new(
        id: "RDGS002",
        suppressedDiagnosticId: "IL3050",
        justification: "The target method has been intercepted by a statically generated variant.");

    public override void ReportSuppressions(SuppressionAnalysisContext context)
    {
        foreach (var diagnostic in context.ReportedDiagnostics)
        {
            if (diagnostic.Id != SuppressRDCDiagnostic.SuppressedDiagnosticId && diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId)
            {
                continue;
            }

            var location = diagnostic.AdditionalLocations.Count > 0
                ? diagnostic.AdditionalLocations[0]
                : diagnostic.Location;

            if (location.SourceTree is not { } sourceTree)
            {
                continue;
            }

            // The trim analyzer changed from warning on the InvocationExpression to the MemberAccessExpression in https://github.com/dotnet/runtime/pull/110086.
            // To account for this, we need to check if the location is an InvocationExpression or a child of an InvocationExpression.
            var node = sourceTree.GetRoot().FindNode(location.SourceSpan) switch
            {
                InvocationExpressionSyntax s => s,
                { Parent: InvocationExpressionSyntax s } => s,
                _ => null,
            };

            if (node is null
                || !node.TryGetMapMethodName(out var method)
                || !InvocationOperationExtensions.KnownMethods.Contains(method))
            {
                continue;
            }

            var semanticModel = context.GetSemanticModel(sourceTree);
            var operation = semanticModel.GetOperation(node, context.CancellationToken);
            var wellKnownTypes = WellKnownTypes.GetOrCreate(semanticModel.Compilation);
            if (operation.IsValidOperation(wellKnownTypes, out var invocationOperation))
            {
                var endpoint = new Endpoint(invocationOperation, wellKnownTypes, semanticModel);
                if (endpoint.Diagnostics.Count == 0)
                {
                    var targetSuppression = diagnostic.Id == SuppressRUCDiagnostic.SuppressedDiagnosticId
                        ? SuppressRUCDiagnostic
                        : SuppressRDCDiagnostic;
                    context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic));
                }
            }
        }
    }
    public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(SuppressRUCDiagnostic, SuppressRDCDiagnostic);
}
