using System.Net.Http.Headers;
using System.Net.WebSockets;
using DodoSSH.Contracts;
using DodoSSH.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Testcontainers.PostgreSql;
using Xunit;

namespace DodoSSH.Api.Tests;

/// <summary>
/// Hosts the API in-process against a real PostgreSQL container and a stubbed identity provider.
/// </summary>
/// <remarks>
/// One container and one host per assembly. Tests therefore use distinct users and vaults rather
/// than assuming an empty database.
/// </remarks>
public sealed class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer container = new PostgreSqlBuilder("postgres:18-alpine")
        .WithDatabase("dodossh")
        .WithUsername("postgres")
        .WithPassword("test")
        .Build();

    /// <summary>The stubbed identity provider.</summary>
    public StubIdentityProvider IdentityProvider { get; } = new();

    /// <inheritdoc />
    public async ValueTask InitializeAsync()
    {
        await container.StartAsync();

        await using var scope = Services.CreateAsyncScope();
        var database = scope.ServiceProvider.GetRequiredService<DodoDbContext>();
        await database.Database.MigrateAsync();
    }

    /// <inheritdoc />
    public override async ValueTask DisposeAsync()
    {
        await base.DisposeAsync();
        await container.DisposeAsync();
        IdentityProvider.Dispose();
    }

    /// <inheritdoc />
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        ArgumentNullException.ThrowIfNull(builder);

        builder.UseEnvironment("Testing");

        // Environment variables rather than ConfigureAppConfiguration. Under minimal hosting the
        // application's own configuration sources are added inside Program.cs after this callback
        // runs, so an appsettings.json value would win over anything registered here. Program.cs
        // adds AddEnvironmentVariables("DODOSSH_") last, which makes these authoritative.
        var settings = new Dictionary<string, string?>(StringComparer.Ordinal)
        {
            ["DODOSSH_ConnectionStrings__Postgres"] = container.GetConnectionString(),
            ["DODOSSH_Server__PublicBaseUrl"] = "http://localhost",
            ["DODOSSH_Oidc__Authority"] = IdentityProvider.Authority,
            ["DODOSSH_Oidc__Audience"] = StubIdentityProvider.Audience,
            ["DODOSSH_Oidc__ClientId"] = "dodossh-desktop",

            // The stub serves plaintext HTTP on a loopback port.
            ["DODOSSH_Oidc__RequireHttpsMetadata"] = "false",

            ["DODOSSH_Relay__Enabled"] = "false",

            // Fixed so cursors stay valid for the lifetime of the test host.
            ["DODOSSH_Sync__CursorSigningKey"] = Convert.ToBase64String(new byte[32]),
        };

        foreach (var (name, value) in settings)
        {
            Environment.SetEnvironmentVariable(name, value);
        }
    }

    /// <summary>Creates a client carrying a valid token for the given subject.</summary>
    /// <remarks>
    /// There is no verified-address knob here, and its absence is worth a sentence. It used to exist so
    /// a test could present the one shape the server refused — an address the provider had not vouched
    /// for, offered against a pending team invitation. Nothing in the server reads
    /// <c>email_verified</c> now, because nothing decides access from an address at all, so a parameter
    /// here would be one that changes no outcome.
    /// </remarks>
    public HttpClient CreateClientFor(string subject, string? email = null)
    {
        var client = CreateClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Bearer",
            IdentityProvider.MintToken(subject, email));

        return client;
    }

    /// <summary>Creates a client carrying the supplied raw token.</summary>
    public HttpClient CreateClientWithToken(string token)
    {
        var client = CreateClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return client;
    }

    /// <summary>Opens a database scope for arranging state and asserting on it.</summary>
    public AsyncServiceScope CreateScope() => Services.CreateAsyncScope();

    /// <summary>
    /// Opens the event socket as the given subject, through the real pipeline.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The bearer token goes on the upgrade request, which is the whole of the socket's authorization
    /// — see ADR 0012 — so a test that stubbed it would be testing nothing. The subprotocol is offered
    /// because the server refuses an upgrade that does not, and that refusal is itself under test.
    /// </para>
    /// <para>
    /// <c>TestServer</c> speaks WebSockets in-memory with no port and no network, so these run
    /// wherever the rest of the suite does.
    /// </para>
    /// </remarks>
    public Task<WebSocket> ConnectEventsAsync(string subject, CancellationToken cancellationToken)
    {
        var token = IdentityProvider.MintToken(subject);
        var client = Server.CreateWebSocketClient();

        client.SubProtocols.Add(VaultEvents.SubProtocol);
        // The server-side request, so the header is a raw string rather than a typed value.
        client.ConfigureRequest = request => request.Headers.Authorization = $"Bearer {token}";

        return client.ConnectAsync(
            new Uri(Server.BaseAddress, VaultEvents.Path.TrimStart('/')),
            cancellationToken);
    }
}

/// <summary>Shares one host and container across every test class in the assembly.</summary>
[CollectionDefinition(Name)]
public sealed class ApiCollection : ICollectionFixture<ApiFixture>
{
    /// <summary>Collection name.</summary>
    public const string Name = "api";
}
