// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Trace;
using TestApp.AspNetCore;
using Xunit;

namespace OpenTelemetry.Instrumentation.AspNetCore.Tests;

[Collection("AspNetCore")]
public class IncomingRequestsCollectionsIsAccordingToTheSpecTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;

    public IncomingRequestsCollectionsIsAccordingToTheSpecTests(WebApplicationFactory<Program> factory)
    {
        this.factory = factory;
    }

    [Theory]
    [InlineData("/api/values", null, "user-agent", 200)]
    [InlineData("/api/values", null, null, 200)]
    [InlineData("/api/exception", null, null, 503)]
    [InlineData("/api/exception", null, null, 503, true)]
    public async Task SuccessfulTemplateControllerCallGeneratesASpan_New(
        string urlPath,
        string? query,
        string? userAgent,
        int statusCode,
        bool recordException = false)
    {
        var exportedItems = new List<Activity>();

        // Arrange
        using (var client = this.factory
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureTestServices((IServiceCollection services) =>
                {
                    services.AddSingleton<TestCallbackMiddleware>(new ExceptionTestCallbackMiddleware(statusCode));
                    services.AddOpenTelemetry()
                        .WithTracing(builder => builder
                            .AddAspNetCoreInstrumentation(options =>
                            {
                                options.RecordException = recordException;
                            })
                            .AddInMemoryExporter(exportedItems));
                });
                builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
            })
            .CreateClient())
        {
            try
            {
                if (!string.IsNullOrEmpty(userAgent))
                {
                    client.DefaultRequestHeaders.Add("User-Agent", userAgent);
                }

                // Act
                var path = urlPath;
                if (query != null)
                {
                    path += query;
                }

                using var response = await client.GetAsync(new Uri(path, UriKind.Relative));
            }
            catch (Exception)
            {
                // ignore errors
            }

            for (var i = 0; i < 10; i++)
            {
                if (exportedItems.Count == 1)
                {
                    break;
                }

                // We need to let End callback execute as it is executed AFTER response was returned.
                // In unit tests environment there may be a lot of parallel unit tests executed, so
                // giving some breezing room for the End callback to complete
                await Task.Delay(TimeSpan.FromSeconds(1));
            }
        }

        Assert.Single(exportedItems);
        var activity = exportedItems[0];

        Assert.Equal(ActivityKind.Server, activity.Kind);
        Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress));
        Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod));
        Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion));
        Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme));
        Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeUrlPath));
        Assert.Equal(query, activity.GetTagValue(SemanticConventions.AttributeUrlQuery));
        Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode));

        if (statusCode == 503)
        {
            Assert.Equal(ActivityStatusCode.Error, activity.Status);
            Assert.Equal("System.Exception", activity.GetTagValue(SemanticConventions.AttributeErrorType));
        }
        else
        {
            Assert.Equal(ActivityStatusCode.Unset, activity.Status);
        }

        // Instrumentation is not expected to set status description
        // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode
        Assert.Null(activity.StatusDescription);

        if (recordException)
        {
            Assert.Single(activity.Events);
            Assert.Equal("exception", activity.Events.First().Name);
        }

        ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent);

        activity.Dispose();
    }

    private static void ValidateTagValue(Activity activity, string attribute, string? expectedValue)
    {
        if (string.IsNullOrEmpty(expectedValue))
        {
            Assert.Null(activity.GetTagValue(attribute));
        }
        else
        {
            Assert.Equal(expectedValue, activity.GetTagValue(attribute));
        }
    }

    internal class ExceptionTestCallbackMiddleware : TestCallbackMiddleware
    {
        private readonly int statusCode;

        public ExceptionTestCallbackMiddleware(int statusCode)
        {
            this.statusCode = statusCode;
        }

        public override async Task<bool> ProcessAsync(HttpContext context)
        {
            context.Response.StatusCode = this.statusCode;
            await context.Response.WriteAsync("empty");

            if (context.Request.Path.HasValue && context.Request.Path!.Value.EndsWith("exception", StringComparison.Ordinal))
            {
                throw new Exception("exception description");
            }

            return false;
        }
    }
}
