using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Books.Api.Tests.Helpers;

/// <summary>
/// Helper client for executing GraphQL queries and mutations in tests.
/// </summary>
public class GraphQLTestClient(HttpClient httpClient)
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    /// <summary>
    /// Executes a GraphQL query and returns the typed result.
    /// </summary>
    public async Task<GraphQLResponse<T>> QueryAsync<T>(
        string query,
        object? variables = null,
        CancellationToken cancellationToken = default)
    {
        return await ExecuteAsync<T>(query, variables, cancellationToken);
    }

    /// <summary>
    /// Executes a GraphQL mutation and returns the typed result.
    /// </summary>
    public async Task<GraphQLResponse<T>> MutateAsync<T>(
        string mutation,
        object? variables = null,
        CancellationToken cancellationToken = default)
    {
        return await ExecuteAsync<T>(mutation, variables, cancellationToken);
    }

    /// <summary>
    /// Executes a GraphQL operation and returns the raw JSON response.
    /// </summary>
    public async Task<string> ExecuteRawAsync(
        string query,
        object? variables = null,
        CancellationToken cancellationToken = default)
    {
        var request = new GraphQLRequest(query, variables);
        var json = JsonSerializer.Serialize(request, JsonOptions);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        var response = await httpClient.PostAsync("/graphql", content, cancellationToken);
        return await response.Content.ReadAsStringAsync(cancellationToken);
    }

    private async Task<GraphQLResponse<T>> ExecuteAsync<T>(
        string query,
        object? variables,
        CancellationToken cancellationToken)
    {
        var request = new GraphQLRequest(query, variables);
        var json = JsonSerializer.Serialize(request, JsonOptions);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        var response = await httpClient.PostAsync("/graphql", content, cancellationToken);
        var responseJson = await response.Content.ReadAsStringAsync(cancellationToken);

        var result = JsonSerializer.Deserialize<GraphQLResponse<T>>(responseJson, JsonOptions);
        return result ?? throw new InvalidOperationException("Failed to deserialize GraphQL response");
    }
}

public record GraphQLRequest(
    [property: JsonPropertyName("query")] string Query,
    [property: JsonPropertyName("variables")] object? Variables = null);

public class GraphQLResponse<T>
{
    [JsonPropertyName("data")]
    public T? Data { get; set; }

    [JsonPropertyName("errors")]
    public List<GraphQLError>? Errors { get; set; }

    public bool HasErrors => Errors?.Count > 0;

    public void EnsureNoErrors()
    {
        if (HasErrors)
        {
            var errorDetails = Errors!.Select(e =>
            {
                var msg = e.Message;
                if (e.Extensions != null)
                {
                    foreach (var ext in e.Extensions)
                    {
                        msg += $"{Environment.NewLine}  [{ext.Key}]: {ext.Value}";
                    }
                }
                return msg;
            });
            var messages = string.Join(Environment.NewLine, errorDetails);
            throw new GraphQLException($"GraphQL errors occurred:{Environment.NewLine}{messages}", Errors!);
        }
    }
}

public class GraphQLError
{
    [JsonPropertyName("message")]
    public string Message { get; set; } = string.Empty;

    [JsonPropertyName("path")]
    public List<object>? Path { get; set; }

    [JsonPropertyName("extensions")]
    public Dictionary<string, object>? Extensions { get; set; }
}

public class GraphQLException(string message, List<GraphQLError> errors) : Exception(message)
{
    public List<GraphQLError> Errors { get; } = errors;
}
