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.Services.Logging;
using ROLAC.API.Services.Notifications;
using Xunit;

namespace ROLAC.API.Tests.Services.Notifications;

public class EmailServiceTests
{
    // Records every email it is asked to send; can be told to throw for a given address.
    private sealed class FakeSmtpDispatcher : ISmtpDispatcher
    {
        public List<OutboundEmail> Sent { get; } = new();
        public string? FailForAddress { get; set; }

        public Task SendAsync(OutboundEmail email, CancellationToken ct = default)
        {
            if (email.ToAddress == FailForAddress)
                throw new InvalidOperationException("smtp rejected");
            Sent.Add(email);
            return Task.CompletedTask;
        }
    }

    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, string? email)
    {
        var member = new Member { FirstName_en = "Test", LastName_en = "User", Email = email };
        db.Members.Add(member);
        await db.SaveChangesAsync();
        return member.Id;
    }

    [Fact]
    public async Task SendAsync_ResolvesMemberEmails_MergesRawAddresses_AndDedupes()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db, "member@example.com");
        var dispatcher = new FakeSmtpDispatcher();
        var service = new EmailService(db, dispatcher, BuildAccessor());

        var message = new EmailMessage(
            MemberIds: new[] { memberId },
            Addresses: new[] { "extra@example.com", "member@example.com" }, // dup of member email
            Subject: "Hi", HtmlBody: "<p>Body</p>");

        var result = await service.SendAsync(message);

        Assert.Equal(2, result.SentCount);          // member@ + extra@, dup dropped
        Assert.Equal(0, result.FailedCount);
        Assert.Equal(2, dispatcher.Sent.Count);
        Assert.Equal(2, await db.NotificationLogs.CountAsync(l => l.Status == NotificationStatuses.Sent));
    }

    [Fact]
    public async Task SendAsync_SkipsMembersWithNoEmail()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db, null);
        var dispatcher = new FakeSmtpDispatcher();
        var service = new EmailService(db, dispatcher, BuildAccessor());

        var result = await service.SendAsync(new EmailMessage(
            new[] { memberId }, Array.Empty<string>(), "Hi", "<p>Body</p>"));

        Assert.Equal(0, result.SentCount);
        Assert.Empty(dispatcher.Sent);
    }

    [Fact]
    public async Task SendAsync_LogsFailure_WithoutAbortingBatch()
    {
        using var db = BuildDb();
        var dispatcher = new FakeSmtpDispatcher { FailForAddress = "bad@example.com" };
        var service = new EmailService(db, dispatcher, BuildAccessor());

        var result = await service.SendAsync(new EmailMessage(
            Array.Empty<int>(),
            new[] { "bad@example.com", "good@example.com" },
            "Hi", "<p>Body</p>"));

        Assert.Equal(1, result.SentCount);
        Assert.Equal(1, result.FailedCount);
        Assert.Single(result.Failures);
        Assert.Equal("bad@example.com", result.Failures[0].Target);
        Assert.Equal(1, await db.NotificationLogs.CountAsync(l => l.Status == NotificationStatuses.Failed));
    }
}
