using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Meezi.API.Middleware;
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Data;
using Xunit;

namespace Meezi.API.Tests;

public class IdempotencyMiddlewareTests
{
    private sealed class TestTenant(string? cafeId) : ITenantContext
    {
        public string? UserId => "user-1";
        public string? CafeId => cafeId;
        public EmployeeRole? Role => EmployeeRole.Owner;
        public PlanTier? PlanTier => Core.Enums.PlanTier.Pro;
        public string? Language => "fa";
        public string? BranchId => null;
        public bool IsSystemAdmin => false;
        public bool IsAuthenticated => true;
    }

    /// <summary>A scope factory whose scopes share one in-memory database, mirroring how the
    /// middleware opens isolated DI scopes against the same store in production.</summary>
    private static IServiceScopeFactory BuildScopeFactory()
    {
        var dbName = Guid.NewGuid().ToString();
        var services = new ServiceCollection();
        services.AddDbContext<AppDbContext>(o => o.UseInMemoryDatabase(dbName));
        services.AddLogging();
        return services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>();
    }

    private static DefaultHttpContext NewPost(string? key)
    {
        var ctx = new DefaultHttpContext();
        ctx.Request.Method = "POST";
        ctx.Request.Path = "/api/test";
        if (key is not null) ctx.Request.Headers["Idempotency-Key"] = key;
        ctx.Response.Body = new MemoryStream();
        return ctx;
    }

    private static string ReadBody(HttpContext ctx)
    {
        ctx.Response.Body.Position = 0;
        return new StreamReader(ctx.Response.Body).ReadToEnd();
    }

    [Fact]
    public async Task SameKey_ExecutesOnce_AndReplaysStoredResponse()
    {
        var scopeFactory = BuildScopeFactory();
        var tenant = new TestTenant("cafe-1");
        var calls = 0;
        RequestDelegate next = async ctx =>
        {
            calls++;
            ctx.Response.StatusCode = 200;
            await ctx.Response.WriteAsync($"{{\"v\":\"{Guid.NewGuid():N}\"}}");
        };
        var mw = new IdempotencyMiddleware(next, NullLogger<IdempotencyMiddleware>.Instance);

        var c1 = NewPost("KEY-1");
        await mw.InvokeAsync(c1, tenant, scopeFactory);
        var body1 = ReadBody(c1);

        var c2 = NewPost("KEY-1");
        await mw.InvokeAsync(c2, tenant, scopeFactory);
        var body2 = ReadBody(c2);

        Assert.Equal(1, calls);                 // executed exactly once
        Assert.Equal(body1, body2);             // second call replays the stored body verbatim
        Assert.Equal(200, c2.Response.StatusCode);
        Assert.Equal("true", c2.Response.Headers["Idempotent-Replay"].ToString());
    }

    [Fact]
    public async Task DifferentKey_ExecutesAgain()
    {
        var scopeFactory = BuildScopeFactory();
        var tenant = new TestTenant("cafe-1");
        var calls = 0;
        RequestDelegate next = async ctx =>
        {
            calls++;
            ctx.Response.StatusCode = 200;
            await ctx.Response.WriteAsync("{\"ok\":true}");
        };
        var mw = new IdempotencyMiddleware(next, NullLogger<IdempotencyMiddleware>.Instance);

        await mw.InvokeAsync(NewPost("A"), tenant, scopeFactory);
        await mw.InvokeAsync(NewPost("B"), tenant, scopeFactory);

        Assert.Equal(2, calls);
    }

    [Fact]
    public async Task NoKey_PassesThrough_NoIdempotency()
    {
        var scopeFactory = BuildScopeFactory();
        var tenant = new TestTenant("cafe-1");
        var calls = 0;
        RequestDelegate next = ctx =>
        {
            calls++;
            ctx.Response.StatusCode = 200;
            return Task.CompletedTask;
        };
        var mw = new IdempotencyMiddleware(next, NullLogger<IdempotencyMiddleware>.Instance);

        await mw.InvokeAsync(NewPost(null), tenant, scopeFactory);
        await mw.InvokeAsync(NewPost(null), tenant, scopeFactory);

        Assert.Equal(2, calls);
    }

    [Fact]
    public async Task SameKey_DifferentTenant_IsNotReplayed()
    {
        var scopeFactory = BuildScopeFactory();
        var calls = 0;
        RequestDelegate next = async ctx =>
        {
            calls++;
            ctx.Response.StatusCode = 200;
            await ctx.Response.WriteAsync("{\"ok\":true}");
        };
        var mw = new IdempotencyMiddleware(next, NullLogger<IdempotencyMiddleware>.Instance);

        await mw.InvokeAsync(NewPost("SHARED"), new TestTenant("cafe-A"), scopeFactory);
        await mw.InvokeAsync(NewPost("SHARED"), new TestTenant("cafe-B"), scopeFactory);

        Assert.Equal(2, calls); // keys are scoped per café — no cross-tenant collision
    }

    [Fact]
    public async Task ServerError_IsNotCached_SoRetryReexecutes()
    {
        var scopeFactory = BuildScopeFactory();
        var tenant = new TestTenant("cafe-1");
        var calls = 0;
        RequestDelegate next = async ctx =>
        {
            calls++;
            ctx.Response.StatusCode = 500;
            await ctx.Response.WriteAsync("{\"error\":\"boom\"}");
        };
        var mw = new IdempotencyMiddleware(next, NullLogger<IdempotencyMiddleware>.Instance);

        await mw.InvokeAsync(NewPost("KEY-5XX"), tenant, scopeFactory);
        var c2 = NewPost("KEY-5XX");
        await mw.InvokeAsync(c2, tenant, scopeFactory);

        Assert.Equal(2, calls); // 5xx is transient → reservation released, retry runs again
        Assert.NotEqual("true", c2.Response.Headers["Idempotent-Replay"].ToString());
    }
}
