using System.Collections.Specialized;
using System.Web;

namespace DodoSSH.Client.Auth.Tests;

/// <summary>
/// Stands in for the system browser, and actually fetches the loopback redirect.
/// </summary>
/// <remarks>
/// <para>
/// Genuinely drives the client's own <see cref="LoopbackCallbackListener"/> over TCP rather than
/// injecting a callback result. The listener's request parsing, path filtering and response writing
/// are all part of what could break, and none of it is exercised by handing the flow a fabricated
/// code.
/// </para>
/// <para>
/// The redirect is fetched on a background task, not awaited inside <see cref="OpenAsync"/>. The
/// client awaits <c>OpenAsync</c> before it begins accepting, so fetching inline would deadlock: the
/// browser would be waiting for a response that only arrives once the client starts listening.
/// </para>
/// </remarks>
internal sealed class FakeBrowser : IBrowserLauncher
{
    private readonly Func<NameValueCollection, IReadOnlyDictionary<string, string>>? buildCallback;

    /// <param name="buildCallback">
    /// Produces the callback query parameters from the authorization request's parameters. Defaults
    /// to a successful code response echoing the state back.
    /// </param>
    internal FakeBrowser(
        Func<NameValueCollection, IReadOnlyDictionary<string, string>>? buildCallback = null) =>
        this.buildCallback = buildCallback;

    /// <summary>The authorization URL the client asked to open.</summary>
    internal Uri? OpenedUrl { get; private set; }

    /// <summary>The authorization request's query parameters.</summary>
    internal NameValueCollection AuthorizeParameters =>
        HttpUtility.ParseQueryString(OpenedUrl?.Query ?? string.Empty);

    /// <summary>The background fetch, so a test can surface its failures.</summary>
    internal Task? CallbackDelivery { get; private set; }

    /// <inheritdoc />
    public Task OpenAsync(Uri url, CancellationToken cancellationToken)
    {
        OpenedUrl = url;

        var parameters = HttpUtility.ParseQueryString(url.Query);
        var redirectUri = parameters["redirect_uri"]
            ?? throw new InvalidOperationException("The authorization URL carried no redirect_uri.");

        var callback = buildCallback is null
            ? new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["code"] = "authorization-code",
                ["state"] = parameters["state"] ?? string.Empty,
            }
            : buildCallback(parameters);

        CallbackDelivery = Task.Run(() => FetchAsync(redirectUri, callback), cancellationToken);

        return Task.CompletedTask;
    }

    private static async Task FetchAsync(
        string redirectUri,
        IReadOnlyDictionary<string, string> callback)
    {
        var query = string.Join(
            '&',
            callback.Select(p =>
                $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));

        using var client = new HttpClient();

        // A real browser asks for this first. The listener must ignore it rather than treating it as
        // the callback, so it is part of the flow under test.
        try
        {
            using var favicon = await client.GetAsync(new Uri($"{redirectUri}/../favicon.ico"));
        }
        catch (HttpRequestException)
        {
            // The listener closes the connection after answering; a transport-level failure here is
            // not what the test is about.
        }

        using var response = await client.GetAsync(new Uri($"{redirectUri}?{query}"));
        _ = await response.Content.ReadAsStringAsync();
    }
}
