using System.Buffers.Text;
using System.Text;
using System.Text.Json.Nodes;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using WireMock.Settings;

namespace DodoSSH.Client.Auth.Tests;

/// <summary>An identity provider stub: discovery and a token endpoint.</summary>
internal sealed class StubProvider : IDisposable
{
    private readonly WireMockServer server;

    internal StubProvider(
        bool advertiseS256 = true,
        string? issuerOverride = null)
    {
        // Loopback explicitly: WireMock's default listens on every interface, which makes Windows Firewall
        // prompt the first time each test executable runs — per binary path, so a new worktree or
        // configuration asks again. Port 0 still picks a free port and reports it on server.Url.
        server = WireMockServer.Start(new WireMockServerSettings { Urls = ["http://127.0.0.1:0"] });
        Authority = new Uri(server.Url!.TrimEnd('/'), UriKind.Absolute);

        StubDiscovery(advertiseS256, issuerOverride);
    }

    /// <summary>The provider's base URL, which is also its issuer.</summary>
    internal Uri Authority { get; }

    /// <summary>Requests the stub received, so tests can assert on what was sent.</summary>
    internal IReadOnlyList<WireMock.Logging.ILogEntry> Requests => server.LogEntries.ToList();

    /// <summary>Stubs a successful token response.</summary>
    internal void StubTokenResponse(
        string accessToken = "access-token",
        string? refreshToken = "refresh-token",
        string? idTokenNonce = null,
        bool includeIdToken = false,
        int? expiresInSeconds = 3600)
    {
        var body = new JsonObject
        {
            ["access_token"] = accessToken,
            ["token_type"] = "Bearer",
        };

        if (expiresInSeconds is { } seconds)
        {
            body["expires_in"] = seconds;
        }

        if (refreshToken is not null)
        {
            body["refresh_token"] = refreshToken;
        }

        if (includeIdToken)
        {
            body["id_token"] = BuildIdToken(idTokenNonce);
        }

        server
            .Given(Request.Create().WithPath("/connect/token").UsingPost())
            .RespondWith(Response.Create()
                .WithStatusCode(200)
                .WithHeader("Content-Type", "application/json")
                .WithBody(body.ToJsonString()));
    }

    /// <summary>Stubs an error from the token endpoint.</summary>
    internal void StubTokenError(int statusCode, string error, string description)
    {
        var body = new JsonObject
        {
            ["error"] = error,
            ["error_description"] = description,
        };

        server
            .Given(Request.Create().WithPath("/connect/token").UsingPost())
            .RespondWith(Response.Create()
                .WithStatusCode(statusCode)
                .WithHeader("Content-Type", "application/json")
                .WithBody(body.ToJsonString()));
    }

    /// <summary>The form body the token endpoint last received, parsed.</summary>
    internal IReadOnlyDictionary<string, string> LastTokenRequestForm()
    {
        var entries = server.LogEntries
            .Where(e => e.RequestMessage?.Path?
                .EndsWith("/connect/token", StringComparison.Ordinal) == true)
            .ToList();

        if (entries.Count == 0)
        {
            throw new InvalidOperationException("The token endpoint was never called.");
        }

        var body = entries[^1].RequestMessage?.Body ?? string.Empty;
        var form = new Dictionary<string, string>(StringComparer.Ordinal);

        foreach (var pair in body.Split('&', StringSplitOptions.RemoveEmptyEntries))
        {
            var separator = pair.IndexOf('=', StringComparison.Ordinal);
            if (separator < 0)
            {
                continue;
            }

            form[Uri.UnescapeDataString(pair[..separator])] =
                Uri.UnescapeDataString(pair[(separator + 1)..].Replace('+', ' '));
        }

        return form;
    }

    /// <inheritdoc />
    public void Dispose()
    {
        server.Stop();
        server.Dispose();
    }

    /// <summary>
    /// Builds a JWT-shaped ID token with an unverifiable signature.
    /// </summary>
    /// <remarks>
    /// Unsigned on purpose. The client reads the nonce without validating the signature, which OIDC
    /// Core §3.1.3.7 permits for a token received directly from the token endpoint over TLS — so a
    /// real signature here would test nothing the client does.
    /// </remarks>
    private static string BuildIdToken(string? nonce)
    {
        var header = new JsonObject { ["alg"] = "RS256", ["typ"] = "JWT" };
        var payload = new JsonObject { ["sub"] = "alice", ["iss"] = "https://idp.example" };

        if (nonce is not null)
        {
            payload["nonce"] = nonce;
        }

        return string.Join('.',
            Base64Url.EncodeToString(Encoding.UTF8.GetBytes(header.ToJsonString())),
            Base64Url.EncodeToString(Encoding.UTF8.GetBytes(payload.ToJsonString())),
            Base64Url.EncodeToString("not-a-real-signature"u8));
    }

    private void StubDiscovery(bool advertiseS256, string? issuerOverride)
    {
        var document = new JsonObject
        {
            ["issuer"] = issuerOverride ?? Authority.AbsoluteUri.TrimEnd('/'),
            ["authorization_endpoint"] = $"{Authority.AbsoluteUri.TrimEnd('/')}/connect/authorize",
            ["token_endpoint"] = $"{Authority.AbsoluteUri.TrimEnd('/')}/connect/token",
            ["jwks_uri"] = $"{Authority.AbsoluteUri.TrimEnd('/')}/.well-known/jwks.json",
            ["response_types_supported"] = new JsonArray("code"),
            ["id_token_signing_alg_values_supported"] = new JsonArray("RS256"),
        };

        if (advertiseS256)
        {
            document["code_challenge_methods_supported"] = new JsonArray("S256");
        }
        else
        {
            document["code_challenge_methods_supported"] = new JsonArray("plain");
        }

        server
            .Given(Request.Create().WithPath("/.well-known/openid-configuration").UsingGet())
            .RespondWith(Response.Create()
                .WithStatusCode(200)
                .WithHeader("Content-Type", "application/json")
                .WithBody(document.ToJsonString()));
    }
}
