using System.Text.Json;

namespace SatelliteProvider.IntegrationTests;

// AZ-795: shared ProblemDetails / ValidationProblemDetails assertion helper
// for integration tests. Every endpoint that emits a 4xx error MUST produce
// a body matching the contract in
// `_docs/02_document/contracts/api/error-shape.md` (v1.0.0). Tests use this
// helper instead of re-deriving the shape per call site.
public static class ProblemDetailsAssertions
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true
    };

    public static async Task<JsonElement> ReadProblemDetailsAsync(HttpResponseMessage response, string label)
    {
        var contentType = response.Content.Headers.ContentType?.MediaType;
        if (contentType is null || !contentType.Contains("application/problem+json", StringComparison.OrdinalIgnoreCase))
        {
            var body = await response.Content.ReadAsStringAsync();
            throw new Exception(
                $"{label}: expected Content-Type 'application/problem+json', got '{contentType}'. Body: {body}");
        }

        var stream = await response.Content.ReadAsStreamAsync();
        using var doc = await JsonDocument.ParseAsync(stream);
        return doc.RootElement.Clone();
    }

    public static void AssertValidationProblem(
        JsonElement problem,
        int expectedStatus,
        string label,
        string? expectedErrorPath = null,
        string? expectedErrorContains = null)
    {
        if (!problem.TryGetProperty("status", out var statusEl) || statusEl.GetInt32() != expectedStatus)
        {
            throw new Exception(
                $"{label}: expected status={expectedStatus}, got {(statusEl.ValueKind == JsonValueKind.Number ? statusEl.GetInt32().ToString() : "missing")}");
        }

        if (!problem.TryGetProperty("title", out var titleEl) || string.IsNullOrEmpty(titleEl.GetString()))
        {
            throw new Exception($"{label}: expected non-empty 'title', got missing/empty.");
        }

        if (!problem.TryGetProperty("errors", out var errorsEl) || errorsEl.ValueKind != JsonValueKind.Object)
        {
            throw new Exception($"{label}: expected 'errors' object, got {errorsEl.ValueKind}.");
        }

        if (expectedErrorPath is not null)
        {
            if (!errorsEl.TryGetProperty(expectedErrorPath, out var fieldEl) || fieldEl.ValueKind != JsonValueKind.Array)
            {
                throw new Exception(
                    $"{label}: expected errors['{expectedErrorPath}'] array, got {(errorsEl.TryGetProperty(expectedErrorPath, out var raw) ? raw.ValueKind.ToString() : "missing")}. " +
                    $"Available paths: {string.Join(", ", EnumeratePaths(errorsEl))}.");
            }

            if (expectedErrorContains is not null)
            {
                var first = fieldEl.EnumerateArray().FirstOrDefault();
                var firstStr = first.ValueKind == JsonValueKind.String ? first.GetString() : null;
                if (firstStr is null || !firstStr.Contains(expectedErrorContains, StringComparison.OrdinalIgnoreCase))
                {
                    throw new Exception(
                        $"{label}: expected errors['{expectedErrorPath}'][0] to contain '{expectedErrorContains}', got '{firstStr}'.");
                }
            }
        }
    }

    public static void AssertProblemDetails(
        JsonElement problem,
        int expectedStatus,
        string label)
    {
        if (!problem.TryGetProperty("status", out var statusEl) || statusEl.GetInt32() != expectedStatus)
        {
            throw new Exception(
                $"{label}: expected status={expectedStatus}, got {(statusEl.ValueKind == JsonValueKind.Number ? statusEl.GetInt32().ToString() : "missing")}");
        }

        if (!problem.TryGetProperty("title", out var titleEl) || string.IsNullOrEmpty(titleEl.GetString()))
        {
            throw new Exception($"{label}: expected non-empty 'title', got missing/empty.");
        }
    }

    // AZ-808 cycle 8: promoted from per-test-file private helpers (was
    // duplicated in TileInventoryValidationTests + RegionFieldRenameTests +
    // RegionRequestValidationTests) so every validation test points at one
    // source of truth for "is this field-name or substring mentioned anywhere
    // in the errors map?".
    public static void AssertErrorsContainsMention(JsonElement problem, string expectedMention, string label)
    {
        if (!problem.TryGetProperty("errors", out var errorsEl) || errorsEl.ValueKind != JsonValueKind.Object)
        {
            throw new Exception($"{label}: expected 'errors' object in ProblemDetails body.");
        }

        var found = false;
        foreach (var prop in errorsEl.EnumerateObject())
        {
            if (prop.Name.Contains(expectedMention, StringComparison.OrdinalIgnoreCase))
            {
                found = true;
                break;
            }

            foreach (var msg in prop.Value.EnumerateArray())
            {
                if (msg.GetString()?.Contains(expectedMention, StringComparison.OrdinalIgnoreCase) == true)
                {
                    found = true;
                    break;
                }
            }

            if (found) break;
        }

        if (!found)
        {
            var paths = string.Join(", ", errorsEl.EnumerateObject().Select(p => p.Name));
            throw new Exception($"{label}: expected '{expectedMention}' to appear in errors keys or messages. Available paths: {paths}.");
        }
    }

    private static IEnumerable<string> EnumeratePaths(JsonElement errorsEl)
    {
        foreach (var prop in errorsEl.EnumerateObject())
        {
            yield return prop.Name;
        }
    }
}
