using System.Text.Json;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;
using Shouldly;
using Xunit;

namespace DodoSSH.Api.Tests;

/// <summary>
/// The generated OpenAPI document, which ADR 0002 promises to third parties and a future CLI.
/// </summary>
/// <remarks>
/// Generated through <see cref="IOpenApiDocumentProvider"/> rather than fetched from
/// <c>/openapi/v1.json</c>: that route is mapped only in Development and sits behind the deny-by-default
/// policy, so it is not reachable from this fixture. The document is the same one either way.
/// </remarks>
[Collection(ApiCollection.Name)]
public sealed class OpenApiDocumentTests(ApiFixture fixture)
{
    [Theory]
    [InlineData("/api/v1/vaults/{vaultId}/sync/pull", "post", "vaultId")]
    [InlineData("/api/v1/vaults/{vaultId}/sync/push", "post", "vaultId")]
    [InlineData("/api/v1/me/devices/{deviceId}", "delete", "deviceId")]
    public async Task RouteParametersAreDeclared(string path, string verb, string parameterName)
    {
        // These endpoints read their route value with Route<T>("name") rather than binding it onto the
        // request DTO, so nothing in the endpoint signature mentions it and the generator emits the path
        // template with no matching parameter — which is invalid OpenAPI and unusable by any client
        // generator. RouteParameterTransformer puts them back; without it this document silently rots.
        var document = await GenerateAsync();

        var operation = document
            .GetProperty("paths").GetProperty(path).GetProperty(verb);

        var parameters = operation.GetProperty("parameters").EnumerateArray()
            .Where(p => string.Equals(p.GetProperty("in").GetString(), "path", StringComparison.Ordinal))
            .ToArray();

        var declared = parameters.SingleOrDefault(
            p => string.Equals(p.GetProperty("name").GetString(), parameterName, StringComparison.Ordinal));

        declared.ValueKind.ShouldNotBe(JsonValueKind.Undefined, $"{verb} {path} declares no {parameterName}");
        declared.GetProperty("required").GetBoolean().ShouldBeTrue();

        // Recovered from the :guid route constraint, not guessed from the name.
        declared.GetProperty("schema").GetProperty("format").GetString().ShouldBe("uuid");
    }

    [Fact]
    public async Task EveryPathTemplateExpressionHasAParameter()
    {
        // The general form of the rule above: no operation may name a template expression it does not
        // declare. Asserted over the whole document so a new endpoint cannot reintroduce the defect.
        var document = await GenerateAsync();

        var offenders = new List<string>();

        foreach (var path in document.GetProperty("paths").EnumerateObject())
        {
            var expected = path.Name.Split('/')
                .Where(segment => segment.StartsWith('{') && segment.EndsWith('}'))
                .Select(segment => segment[1..^1])
                .ToArray();

            if (expected.Length == 0)
            {
                continue;
            }

            foreach (var operation in path.Value.EnumerateObject())
            {
                var declared = operation.Value.TryGetProperty("parameters", out var parameters)
                    ? parameters.EnumerateArray()
                        .Where(p => string.Equals(p.GetProperty("in").GetString(), "path", StringComparison.Ordinal))
                        .Select(p => p.GetProperty("name").GetString())
                        .ToArray()
                    : [];

                offenders.AddRange(
                    expected.Except(declared, StringComparer.Ordinal)
                        .Select(missing => $"{operation.Name.ToUpperInvariant()} {path.Name} -> {missing}"));
            }
        }

        offenders.ShouldBeEmpty();
    }

    private async Task<JsonElement> GenerateAsync()
    {
        await using var scope = fixture.CreateScope();
        // Keyed on the document name: AddOpenApi registers one provider per document.
        var provider = scope.ServiceProvider.GetRequiredKeyedService<IOpenApiDocumentProvider>("v1");

        var document = await provider.GetOpenApiDocumentAsync(TestContext.Current.CancellationToken);

        var json = await document.SerializeAsJsonAsync(
            OpenApiSpecVersion.OpenApi3_1,
            TestContext.Current.CancellationToken);

        return JsonDocument.Parse(json).RootElement;
    }
}
