using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Moq;
using ROLAC.API.Data;
using ROLAC.API.Data.Interceptors;
using ROLAC.API.Entities;
using ROLAC.API.Entities.Notifications;
using ROLAC.API.Services.Logging;
using ROLAC.API.Services.Notifications;
using Xunit;

namespace ROLAC.API.Tests.Services.Notifications;

public class WebPushServiceTests
{
    // Records the endpoints it was asked to push to; can be told to fail or to report "gone" for one.
    private sealed class FakeWebPushSender : IWebPushSender
    {
        public List<string> SentToEndpoints { get; } = new();
        public string? ExpireForEndpoint { get; set; }
        public string? FailForEndpoint { get; set; }

        public Task<WebPushSendResult> SendAsync(WebPushTarget target, string payloadJson, CancellationToken ct = default)
        {
            if (target.Endpoint == ExpireForEndpoint)
                return Task.FromResult(new WebPushSendResult(Success: false, IsExpired: true, Error: "gone"));
            if (target.Endpoint == FailForEndpoint)
                return Task.FromResult(new WebPushSendResult(Success: false, IsExpired: false, Error: "transient"));
            SentToEndpoints.Add(target.Endpoint);
            return Task.FromResult(new WebPushSendResult(Success: true, IsExpired: false, Error: null));
        }
    }

    private static CurrentUserAccessor BuildAccessor(string userId = "test-user")
    {
        var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId) };
        var ctx    = new DefaultHttpContext { User = new(new ClaimsIdentity(claims)) };
        var mock   = new Mock<IHttpContextAccessor>();
        mock.Setup(x => x.HttpContext).Returns(ctx);
        return new CurrentUserAccessor(mock.Object);
    }

    private static AppDbContext BuildDb()
    {
        var interceptor = new AuditSaveChangesInterceptor(BuildAccessor());
        return new AppDbContext(
            new DbContextOptionsBuilder<AppDbContext>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString())
                .AddInterceptors(interceptor)
                .Options);
    }

    private static async Task<int> SeedMemberAsync(AppDbContext db)
    {
        var member = new Member { FirstName_en = "Test", LastName_en = "User" };
        db.Members.Add(member);
        await db.SaveChangesAsync();
        return member.Id;
    }

    private static async Task SeedSubscriptionAsync(AppDbContext db, int memberId, string endpoint)
    {
        db.WebPushSubscriptions.Add(new WebPushSubscription
        {
            MemberId = memberId, Endpoint = endpoint, P256dh = "key", Auth = "auth", CreatedAt = DateTime.UtcNow,
        });
        await db.SaveChangesAsync();
    }

    private static WebPushPayload Payload() => new("Title", "Body", "/user-portal/finance/expenses");

    [Fact]
    public async Task SendToMembersAsync_PushesToEveryDeviceSubscription_AndLogsEach()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        await SeedSubscriptionAsync(db, memberId, "https://push.example/phone");
        await SeedSubscriptionAsync(db, memberId, "https://push.example/laptop");
        var sender = new FakeWebPushSender();
        var service = new WebPushService(db, sender, BuildAccessor());

        var result = await service.SendToMembersAsync(new[] { memberId }, Payload(), "test-user");

        Assert.Equal(2, result.SentCount);
        Assert.Equal(0, result.FailedCount);
        Assert.Equal(2, sender.SentToEndpoints.Count);
        Assert.Equal(2, await db.NotificationLogs.CountAsync(l =>
            l.Channel == NotificationChannels.WebPush && l.Status == NotificationStatuses.Sent));
    }

    [Fact]
    public async Task SendToMembersAsync_PrunesSubscription_WhenPushServiceReportsGone()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        await SeedSubscriptionAsync(db, memberId, "https://push.example/dead");
        await SeedSubscriptionAsync(db, memberId, "https://push.example/live");
        var sender = new FakeWebPushSender { ExpireForEndpoint = "https://push.example/dead" };
        var service = new WebPushService(db, sender, BuildAccessor());

        var result = await service.SendToMembersAsync(new[] { memberId }, Payload(), "test-user");

        Assert.Equal(1, result.SentCount);
        Assert.Equal(1, result.FailedCount);
        var remaining = await db.WebPushSubscriptions.Select(s => s.Endpoint).ToListAsync();
        Assert.Equal(new[] { "https://push.example/live" }, remaining);
    }

    [Fact]
    public async Task SendToMembersAsync_LogsTransientFailure_WithoutPruning_OrAborting()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        await SeedSubscriptionAsync(db, memberId, "https://push.example/flaky");
        await SeedSubscriptionAsync(db, memberId, "https://push.example/ok");
        var sender = new FakeWebPushSender { FailForEndpoint = "https://push.example/flaky" };
        var service = new WebPushService(db, sender, BuildAccessor());

        var result = await service.SendToMembersAsync(new[] { memberId }, Payload(), "test-user");

        Assert.Equal(1, result.SentCount);
        Assert.Equal(1, result.FailedCount);
        Assert.Single(result.Failures);
        Assert.Equal(2, await db.WebPushSubscriptions.CountAsync());   // transient failure does NOT prune
        Assert.Equal(1, await db.NotificationLogs.CountAsync(l => l.Status == NotificationStatuses.Failed));
    }

    [Fact]
    public async Task SendToMembersAsync_ReturnsEmpty_WhenMemberHasNoSubscription()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        var sender = new FakeWebPushSender();
        var service = new WebPushService(db, sender, BuildAccessor());

        var result = await service.SendToMembersAsync(new[] { memberId }, Payload(), "test-user");

        Assert.Equal(0, result.SentCount);
        Assert.Empty(sender.SentToEndpoints);
        Assert.Equal(0, await db.NotificationLogs.CountAsync());
    }
}
