using System.Text.Json;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Moq;
using SatelliteProvider.Api;

namespace SatelliteProvider.Tests;

public class GlobalExceptionHandlerTests
{
    [Fact]
    public async Task TryHandleAsync_WritesSanitizedProblemDetailsAndReturnsTrue_AC1()
    {
        // Arrange
        var loggerMock = new Mock<ILogger<GlobalExceptionHandler>>();
        var handler = new GlobalExceptionHandler(loggerMock.Object);
        var httpContext = new DefaultHttpContext { TraceIdentifier = "trace-12345" };
        httpContext.Request.Method = "POST";
        httpContext.Request.Path = "/api/satellite/route";
        var body = new MemoryStream();
        httpContext.Response.Body = body;
        var leakySecret = "Connection string Host=secret-db;Password=hunter2 failed at line 42";
        var exception = new InvalidOperationException(leakySecret);

        // Act
        var handled = await handler.TryHandleAsync(httpContext, exception, CancellationToken.None);

        // Assert
        handled.Should().BeTrue();
        httpContext.Response.StatusCode.Should().Be(StatusCodes.Status500InternalServerError);
        httpContext.Response.ContentType.Should().Contain("application/problem+json");

        body.Position = 0;
        using var doc = JsonDocument.Parse(body);
        var root = doc.RootElement;

        root.GetProperty("status").GetInt32().Should().Be(500);
        root.GetProperty("title").GetString().Should().Be("Internal Server Error");
        root.GetProperty("detail").GetString().Should().NotContain("hunter2");
        root.GetProperty("detail").GetString().Should().NotContain("secret-db");
        root.GetProperty("correlationId").GetString().Should().Be("trace-12345");
    }

    [Fact]
    public async Task TryHandleAsync_BadHttpRequestException_HonorsFrameworkStatusAndDoesNotErrorLog_AC3()
    {
        // Arrange
        var loggerMock = new Mock<ILogger<GlobalExceptionHandler>>();
        var handler = new GlobalExceptionHandler(loggerMock.Object);
        var httpContext = new DefaultHttpContext { TraceIdentifier = "trace-bind-fail" };
        httpContext.Response.Body = new MemoryStream();
        var bindFailure = new BadHttpRequestException(
            "Failed to bind parameter \"double Latitude\" from \"abc\".",
            StatusCodes.Status400BadRequest);

        // Act
        var handled = await handler.TryHandleAsync(httpContext, bindFailure, CancellationToken.None);

        // Assert
        handled.Should().BeTrue("the handler writes the response itself rather than promoting to 5xx");
        httpContext.Response.StatusCode.Should().Be(StatusCodes.Status400BadRequest,
            "framework-level request errors must keep their intended 4xx status, not become 500");
        loggerMock.Verify(
            l => l.Log(
                LogLevel.Error,
                It.IsAny<EventId>(),
                It.IsAny<It.IsAnyType>(),
                It.IsAny<Exception?>(),
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Never,
            "BadHttpRequestException is a client error and must not be ERROR-logged as a server failure");
    }

    [Fact]
    public async Task TryHandleAsync_DeserializationFailure_WritesValidationProblemDetailsWithJsonPath_AZ795()
    {
        // Arrange
        var loggerMock = new Mock<ILogger<GlobalExceptionHandler>>();
        var handler = new GlobalExceptionHandler(loggerMock.Object);
        var httpContext = new DefaultHttpContext { TraceIdentifier = "trace-AZ795" };
        httpContext.Response.Body = new MemoryStream();
        var jsonInner = new JsonException(
            "The JSON property 'foo' could not be mapped to any .NET member contained in type 'TileInventoryRequest'.",
            "$.tiles[0].foo",
            lineNumber: null,
            bytePositionInLine: null);
        var bindFailure = new BadHttpRequestException(
            "Failed to read parameter \"TileInventoryRequest request\" from request body.",
            StatusCodes.Status400BadRequest,
            jsonInner);

        // Act
        var handled = await handler.TryHandleAsync(httpContext, bindFailure, CancellationToken.None);

        // Assert
        handled.Should().BeTrue();
        httpContext.Response.StatusCode.Should().Be(StatusCodes.Status400BadRequest);
        httpContext.Response.ContentType.Should().Contain("application/problem+json");

        httpContext.Response.Body.Position = 0;
        using var doc = JsonDocument.Parse(httpContext.Response.Body);
        var root = doc.RootElement;

        root.GetProperty("status").GetInt32().Should().Be(400);
        root.GetProperty("title").GetString().Should().Be("One or more validation errors occurred.");
        root.GetProperty("type").GetString().Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1");
        root.GetProperty("errors")
            .GetProperty("tiles[0].foo")[0]
            .GetString()
            .Should().Be("The field value is invalid.");
        root.GetProperty("errors")
            .GetProperty("tiles[0].foo")[0]
            .GetString()
            .Should().NotContain("TileInventoryRequest");
    }

    [Fact]
    public async Task TryHandleAsync_BadHttpRequestExceptionWithoutJson_UsesStaticDetail()
    {
        // Arrange
        var loggerMock = new Mock<ILogger<GlobalExceptionHandler>>();
        var handler = new GlobalExceptionHandler(loggerMock.Object);
        var httpContext = new DefaultHttpContext { TraceIdentifier = "trace-bind-static" };
        httpContext.Response.Body = new MemoryStream();
        var bindFailure = new BadHttpRequestException(
            "Failed to bind parameter \"double Latitude\" from \"abc\".",
            StatusCodes.Status400BadRequest);

        // Act
        var handled = await handler.TryHandleAsync(httpContext, bindFailure, CancellationToken.None);

        // Assert
        handled.Should().BeTrue();
        httpContext.Response.Body.Position = 0;
        using var doc = JsonDocument.Parse(httpContext.Response.Body);
        doc.RootElement.GetProperty("detail").GetString()
            .Should().Be("The request could not be processed.");
        doc.RootElement.GetProperty("detail").GetString()
            .Should().NotContain("Latitude");
    }

    [Fact]
    public async Task TryHandleAsync_LogsFullExceptionWithCorrelationId_AC2()
    {
        // Arrange
        var loggerMock = new Mock<ILogger<GlobalExceptionHandler>>();
        var handler = new GlobalExceptionHandler(loggerMock.Object);
        var httpContext = new DefaultHttpContext { TraceIdentifier = "trace-AC2" };
        httpContext.Request.Method = "GET";
        httpContext.Request.Path = "/api/satellite/region/abc";
        httpContext.Response.Body = new MemoryStream();
        var exception = new InvalidOperationException("inner failure detail");

        // Act
        await handler.TryHandleAsync(httpContext, exception, CancellationToken.None);

        // Assert
        loggerMock.Verify(
            l => l.Log(
                LogLevel.Error,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((state, _) =>
                    state.ToString()!.Contains("trace-AC2") &&
                    state.ToString()!.Contains("/api/satellite/region/abc") &&
                    state.ToString()!.Contains("GET")),
                exception,
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Once);
    }
}
