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 LineNotificationServiceTests
{
    // Records pushes; can be told to fail every call.
    private sealed class FakeMessageChannel : IMessageChannel
    {
        public List<(string Target, string Text)> UserPushes { get; } = new();
        public List<(string Target, string Text)> GroupPushes { get; } = new();
        public bool Fail { get; set; }

        public Task<MessageSendResult> PushToUserAsync(string externalId, string text, CancellationToken ct = default)
        {
            UserPushes.Add((externalId, text));
            return Task.FromResult(new MessageSendResult(!Fail, Fail ? "boom" : null));
        }
        public Task<MessageSendResult> PushToGroupAsync(string externalId, string text, CancellationToken ct = default)
        {
            GroupPushes.Add((externalId, text));
            return Task.FromResult(new MessageSendResult(!Fail, Fail ? "boom" : null));
        }
        public Task<MessageSendResult> ReplyAsync(string replyToken, string text, CancellationToken ct = default)
            => Task.FromResult(new MessageSendResult(true, 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;
    }

    [Fact]
    public async Task GenerateLineBindingCodeAsync_PersistsUnconsumedCode()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        var service = new LineNotificationService(db, new FakeMessageChannel());

        var code = await service.GenerateLineBindingCodeAsync(memberId);

        var stored = await db.LineBindingCodes.SingleAsync();
        Assert.Equal(code, stored.Code);
        Assert.Null(stored.ConsumedAt);
        Assert.True(stored.ExpiresAt > DateTime.UtcNow);
    }

    [Fact]
    public async Task TryBindMemberAsync_BindsMember_AndConsumesCode()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        var service = new LineNotificationService(db, new FakeMessageChannel());
        var code = await service.GenerateLineBindingCodeAsync(memberId);

        var result = await service.TryBindMemberAsync("U999", code);

        Assert.True(result.Success);
        Assert.Equal(memberId, result.MemberId);
        var binding = await db.MemberChannelBindings.SingleAsync();
        Assert.Equal("U999", binding.ExternalId);
        Assert.NotNull((await db.LineBindingCodes.SingleAsync()).ConsumedAt);
    }

    [Fact]
    public async Task TryBindMemberAsync_Fails_ForExpiredOrUsedOrUnknownCode()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        db.LineBindingCodes.Add(new LineBindingCode
        {
            Code = "EXPIRE", MemberId = memberId, ExpiresAt = DateTime.UtcNow.AddMinutes(-1),
        });
        await db.SaveChangesAsync();
        var service = new LineNotificationService(db, new FakeMessageChannel());

        Assert.False((await service.TryBindMemberAsync("U1", "EXPIRE")).Success); // expired
        Assert.False((await service.TryBindMemberAsync("U1", "NOPE")).Success);   // unknown
        Assert.Empty(await db.MemberChannelBindings.ToListAsync());
    }

    [Fact]
    public async Task TryBindMemberAsync_Rebinds_UpdatesExistingBinding()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        var service = new LineNotificationService(db, new FakeMessageChannel());
        await service.TryBindMemberAsync("U-OLD", await service.GenerateLineBindingCodeAsync(memberId));

        await service.TryBindMemberAsync("U-NEW", await service.GenerateLineBindingCodeAsync(memberId));

        var binding = await db.MemberChannelBindings.SingleAsync();
        Assert.Equal("U-NEW", binding.ExternalId);
    }

    [Fact]
    public async Task RegisterGroupAsync_IsIdempotent_AndDeactivateFlips()
    {
        using var db = BuildDb();
        var service = new LineNotificationService(db, new FakeMessageChannel());

        await service.RegisterGroupAsync("G1");
        await service.RegisterGroupAsync("G1");          // second call must not duplicate
        Assert.Equal(1, await db.MessagingGroups.CountAsync());
        Assert.True((await db.MessagingGroups.SingleAsync()).IsActive);

        await service.DeactivateGroupAsync("G1");
        Assert.False((await db.MessagingGroups.SingleAsync()).IsActive);
    }

    [Fact]
    public async Task SendLineAsync_PushesToBoundMembersAndActiveGroups_AndLogs()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        db.MemberChannelBindings.Add(new MemberChannelBinding
        {
            MemberId = memberId, Channel = "line", ExternalId = "U-MEM", BoundAt = DateTime.UtcNow,
        });
        var activeGroup = new MessagingGroup { Channel = "line", ExternalId = "G-ON", IsActive = true, RegisteredAt = DateTime.UtcNow };
        var deadGroup   = new MessagingGroup { Channel = "line", ExternalId = "G-OFF", IsActive = false, RegisteredAt = DateTime.UtcNow };
        db.MessagingGroups.AddRange(activeGroup, deadGroup);
        await db.SaveChangesAsync();
        var channel = new FakeMessageChannel();
        var service = new LineNotificationService(db, channel);

        var result = await service.SendLineAsync("notice", new[] { memberId },
            new[] { activeGroup.Id, deadGroup.Id }, "admin-1");

        Assert.Equal(2, result.SentCount);                       // member + active group only
        Assert.Single(channel.UserPushes);
        Assert.Single(channel.GroupPushes);                      // inactive group skipped
        Assert.Equal(2, await db.NotificationLogs.CountAsync(l => l.Status == NotificationStatuses.Sent));
    }

    [Fact]
    public async Task SendLineAsync_RecordsFailures_WhenChannelFails()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        db.MemberChannelBindings.Add(new MemberChannelBinding
        {
            MemberId = memberId, Channel = "line", ExternalId = "U-MEM", BoundAt = DateTime.UtcNow,
        });
        await db.SaveChangesAsync();
        var service = new LineNotificationService(db, new FakeMessageChannel { Fail = true });

        var result = await service.SendLineAsync("notice", new[] { memberId }, Array.Empty<int>(), "admin-1");

        Assert.Equal(0, result.SentCount);
        Assert.Equal(1, result.FailedCount);
        Assert.Equal(1, await db.NotificationLogs.CountAsync(l => l.Status == NotificationStatuses.Failed));
    }

    [Fact]
    public async Task SendLineAsync_SkipsSoftDeletedMembers()
    {
        using var db = BuildDb();
        var memberId = await SeedMemberAsync(db);
        db.MemberChannelBindings.Add(new MemberChannelBinding
        {
            MemberId = memberId, Channel = "line", ExternalId = "U-DEL", BoundAt = DateTime.UtcNow,
        });
        await db.SaveChangesAsync();

        // Soft-delete the member.
        var member = await db.Members.FirstAsync(m => m.Id == memberId);
        member.IsDeleted = true;
        await db.SaveChangesAsync();

        var channel = new FakeMessageChannel();
        var service = new LineNotificationService(db, channel);

        var result = await service.SendLineAsync("notice", new[] { memberId }, Array.Empty<int>(), "admin-1");

        Assert.Equal(0, result.SentCount);
        Assert.Empty(channel.UserPushes);
    }
}
