using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Moq;
using ROLAC.API.Data;
using ROLAC.API.Data.Interceptors;
using ROLAC.API.DTOs.Giving;
using ROLAC.API.Entities;
using ROLAC.API.Services;
using ROLAC.API.Services.Storage;
using Xunit;

namespace ROLAC.API.Tests.Services;

public class OfferingSessionServiceTests
{
    // Proof storage is not exercised by these tests; a no-op keeps the service constructible.
    private sealed class NoOpFileStorage : IFileStorage
    {
        public Task<string> SaveAsync(Stream content, string relativePath, CancellationToken ct = default)
            => Task.FromResult(relativePath);
        public Task<Stream?> OpenReadAsync(string relativePath, CancellationToken ct = default)
            => Task.FromResult<Stream?>(null);
        public Task DeleteAsync(string relativePath, CancellationToken ct = default)
            => Task.CompletedTask;
    }

    private static IHttpContextAccessor 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 mock.Object;
    }

    private static AppDbContext BuildDb(string userId = "test-user")
    {
        var interceptor = new AuditSaveChangesInterceptor(new ROLAC.API.Services.Logging.CurrentUserAccessor(BuildAccessor(userId)));
        return new AppDbContext(
            new DbContextOptionsBuilder<AppDbContext>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString())
                .AddInterceptors(interceptor)
                .Options);
    }

    private static async Task<int> SeedCategoryAsync(AppDbContext db)
    {
        var c = new GivingCategory { Name_en = "Tithe", IsActive = true };
        db.GivingCategories.Add(c);
        await db.SaveChangesAsync();
        return c.Id;
    }

    private static CreateOfferingSessionRequest BuildRequest(int catId, DateOnly date) => new()
    {
        SessionDate = date, CashTotal = 150m, CheckTotal = 0m,
        Givings =
        [
            new() { GivingCategoryId = catId, Amount = 100m, PaymentMethod = "Cash" },
            new() { GivingCategoryId = catId, Amount = 50m,  PaymentMethod = "Cash", IsAnonymous = true },
        ],
    };

    [Fact]
    public async Task CreateAsync_RecomputesSystemTotalAndDifference_ServerSide()
    {
        using var db = BuildDb();
        var catId = await SeedCategoryAsync(db);
        var svc = new OfferingSessionService(db, BuildAccessor(), new NoOpFileStorage());

        var id = await svc.CreateAsync(BuildRequest(catId, new DateOnly(2026, 5, 31)));

        var saved = await db.OfferingSessions.FindAsync(id);
        Assert.Equal("Submitted", saved!.Status);
        Assert.Equal(150m, saved.SystemTotal);
        Assert.Equal(0m, saved.Difference);
        Assert.NotNull(saved.SubmittedAt);
        Assert.Equal(2, await db.Givings.CountAsync(g => g.OfferingSessionId == id));
    }

    [Fact]
    public async Task CreateAsync_LinesGetSessionDateAndAnonymousNullsMember()
    {
        using var db = BuildDb();
        var catId = await SeedCategoryAsync(db);
        var svc = new OfferingSessionService(db, BuildAccessor(), new NoOpFileStorage());

        var id = await svc.CreateAsync(BuildRequest(catId, new DateOnly(2026, 5, 31)));

        var lines = await db.Givings.Where(g => g.OfferingSessionId == id).ToListAsync();
        Assert.All(lines, l => Assert.Equal(new DateOnly(2026,5,31), l.GivingDate));
        Assert.Contains(lines, l => l.IsAnonymous && l.MemberId == null);
    }

    [Fact]
    public async Task CreateAsync_Throws_OnDuplicateSessionDate()
    {
        using var db = BuildDb();
        var catId = await SeedCategoryAsync(db);
        var svc = new OfferingSessionService(db, BuildAccessor(), new NoOpFileStorage());
        await svc.CreateAsync(BuildRequest(catId, new DateOnly(2026, 5, 31)));

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            svc.CreateAsync(BuildRequest(catId, new DateOnly(2026, 5, 31))));
    }

    [Fact]
    public async Task ReplaceAsync_Throws_WhenSessionIsSubmitted()
    {
        using var db = BuildDb();
        var catId = await SeedCategoryAsync(db);
        var svc = new OfferingSessionService(db, BuildAccessor(), new NoOpFileStorage());
        var id = await svc.CreateAsync(BuildRequest(catId, new DateOnly(2026, 5, 31)));

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            svc.ReplaceAsync(id, BuildRequest(catId, new DateOnly(2026, 5, 31))));
    }

    [Fact]
    public async Task ReopenThenReplace_SwapsLinesAndResubmits()
    {
        using var db = BuildDb();
        var catId = await SeedCategoryAsync(db);
        var svc = new OfferingSessionService(db, BuildAccessor(), new NoOpFileStorage());
        var id = await svc.CreateAsync(BuildRequest(catId, new DateOnly(2026, 5, 31)));

        await svc.ReopenAsync(id);
        var reopened = await db.OfferingSessions.FindAsync(id);
        Assert.Equal("Draft", reopened!.Status);

        var newReq = new CreateOfferingSessionRequest
        {
            SessionDate = new DateOnly(2026,5,31), CashTotal = 200m, CheckTotal = 0m,
            Givings = [ new() { GivingCategoryId = catId, Amount = 200m, PaymentMethod = "Cash" } ],
        };
        await svc.ReplaceAsync(id, newReq);

        var after = await db.OfferingSessions.FindAsync(id);
        Assert.Equal("Submitted", after!.Status);
        Assert.Equal(200m, after.SystemTotal);
        Assert.Equal(1, await db.Givings.CountAsync(g => g.OfferingSessionId == id));
    }

    [Fact]
    public async Task GetByIdAsync_ReturnsCheckZelleAndPayPalRefs()
    {
        using var db = BuildDb();
        var catId = await SeedCategoryAsync(db);
        var svc = new OfferingSessionService(db, BuildAccessor(), new NoOpFileStorage());
        var req = new CreateOfferingSessionRequest
        {
            SessionDate = new DateOnly(2026, 6, 7), CashTotal = 0m, CheckTotal = 100m,
            Givings = [ new() { GivingCategoryId = catId, Amount = 100m, PaymentMethod = "Zelle",
                ZelleReferenceCode = "Z-123", PayPalTransactionId = "PP-456", CheckNumber = "C-789" } ],
        };
        var id = await svc.CreateAsync(req);

        var dto = await svc.GetByIdAsync(id);

        Assert.NotNull(dto);
        var line = Assert.Single(dto!.Givings);
        Assert.Equal("Z-123", line.ZelleReferenceCode);
        Assert.Equal("PP-456", line.PayPalTransactionId);
        Assert.Equal("C-789", line.CheckNumber);
    }

    [Fact]
    public async Task GetPagedAsync_IncludesSundayAttendanceTotal_WhenRowExists()
    {
        using var db = BuildDb();
        var catId = await SeedCategoryAsync(db);
        var svc = new OfferingSessionService(db, BuildAccessor(), new NoOpFileStorage());

        var withDate    = new DateOnly(2026, 5, 31);
        var withoutDate = new DateOnly(2026, 5, 24);
        await svc.CreateAsync(BuildRequest(catId, withDate));
        await svc.CreateAsync(BuildRequest(catId, withoutDate));
        db.MealAttendances.Add(new MealAttendance
            { AttendanceDate = withDate, AdultCount = 40, YouthCount = 12, KidCount = 8 });
        await db.SaveChangesAsync();

        var page = await svc.GetPagedAsync(1, 20, null, null);

        var withItem    = page.Items.Single(i => i.SessionDate == "2026-05-31");
        var withoutItem = page.Items.Single(i => i.SessionDate == "2026-05-24");
        Assert.Equal(60, withItem.SundayAttendanceCount);   // 40 + 12 + 8
        Assert.Null(withoutItem.SundayAttendanceCount);      // no attendance row -> null
    }
}
