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

namespace DodoSSH.Client.Api.Tests;

/// <summary>A stand-in DodoSSH server.</summary>
/// <remarks>
/// Responses are built from the real contract types and the real source-generated serialiser, so the
/// client is reading exactly the shape a live server produces rather than hand-written JSON that
/// happens to satisfy it.
/// </remarks>
internal sealed class StubServer : IDisposable
{
    /// <remarks>
    /// Bound to loopback explicitly. WireMock's default listens on every interface, which makes Windows
    /// Firewall prompt the first time each test executable runs — and the prompt is per binary path, so a
    /// new worktree or configuration asks again. Port 0 still picks a free port and reports it on
    /// <see cref="WireMockServer.Url"/>.
    /// </remarks>
    private readonly WireMockServer server = WireMockServer.Start(
        new WireMockServerSettings { Urls = ["http://127.0.0.1:0"] });

    internal Uri BaseUrl => new(server.Url!, UriKind.Absolute);

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

    internal void StubMe(MeResponse response) =>
        StubJson("/api/v1/me", "GET", 200, JsonSerializer.Serialize(
            response, DodoSshJsonContext.Default.MeResponse));

    internal void StubEnrollment(EnrollmentResponse response) =>
        StubJson("/api/v1/me/enrollment", "POST", 200, JsonSerializer.Serialize(
            response, DodoSshJsonContext.Default.EnrollmentResponse));

    internal void StubMeta(MetaResponse response) =>
        StubJson("/api/v1/meta", "GET", 200, JsonSerializer.Serialize(
            response, DodoSshJsonContext.Default.MetaResponse));

    internal void StubPush(Guid vaultId, SyncPushResponse response) =>
        StubJson($"/api/v1/vaults/{vaultId}/sync/push", "POST", 200, JsonSerializer.Serialize(
            response, DodoSshJsonContext.Default.SyncPushResponse));

    internal void StubPull(Guid vaultId, SyncPullResponse response) =>
        StubJson($"/api/v1/vaults/{vaultId}/sync/pull", "POST", 200, JsonSerializer.Serialize(
            response, DodoSshJsonContext.Default.SyncPullResponse));

    /// <summary>Stubs an RFC 9457 problem response.</summary>
    internal void StubProblem(string path, string method, int statusCode, string code, string detail)
    {
        var problem = new JsonObject
        {
            ["type"] = ProblemCodes.TypeBaseUri + code,
            ["title"] = "Request failed",
            ["status"] = statusCode,
            ["detail"] = detail,
            ["code"] = code,
        };

        StubJson(path, method, statusCode, problem.ToJsonString());
    }

    /// <summary>Stubs a non-JSON error, as a reverse proxy in front of a dead server would return.</summary>
    internal void StubGatewayError(string path, string method) =>
        server
            .Given(Request.Create().WithPath(path).UsingMethod(method))
            .RespondWith(Response.Create()
                .WithStatusCode(502)
                .WithHeader("Content-Type", "text/html")
                .WithBody("<html><body><h1>502 Bad Gateway</h1></body></html>"));

    /// <summary>The body of the last request to a path.</summary>
    internal string LastBody(string path)
    {
        var entries = server.LogEntries
            .Where(e => e.RequestMessage?.Path?.EndsWith(path, StringComparison.Ordinal) == true)
            .ToList();

        if (entries.Count == 0)
        {
            throw new InvalidOperationException($"Nothing was sent to {path}.");
        }

        return entries[^1].RequestMessage?.Body ?? string.Empty;
    }

    /// <summary>The Authorization header of the last request to a path.</summary>
    internal string? LastAuthorization(string path)
    {
        var entries = server.LogEntries
            .Where(e => e.RequestMessage?.Path?.EndsWith(path, StringComparison.Ordinal) == true)
            .ToList();

        if (entries.Count == 0)
        {
            return null;
        }

        var headers = entries[^1].RequestMessage?.Headers;

        return headers is not null && headers.TryGetValue("Authorization", out var values)
            ? values.FirstOrDefault()
            : null;
    }

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

    private void StubJson(string path, string method, int statusCode, string body) =>
        server
            .Given(Request.Create().WithPath(path).UsingMethod(method))
            .RespondWith(Response.Create()
                .WithStatusCode(statusCode)
                .WithHeader("Content-Type", "application/json")
                .WithBody(body));
}

/// <summary>Hands out a fixed token, so tests can assert it reached the wire.</summary>
internal sealed class StubTokenProvider(string token = "test-access-token") : IAccessTokenProvider
{
    /// <inheritdoc />
    public ValueTask<string> GetAccessTokenAsync(CancellationToken cancellationToken) =>
        ValueTask.FromResult(token);
}
