using Baya.Application.Contracts.Common;
using Baya.Application.Features.Messaging.Commands.PostMessage;
using Baya.Application.Features.Messaging.Queries.GetTicketThread;
using Baya.Domain.Entities.Messaging;
using Baya.Domain.Entities.User;
using Baya.Infrastructure.Persistence;
using Baya.Infrastructure.Persistence.Repositories.Common;
using Baya.Tests.Setup.Setups;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using NSubstitute;

namespace Baya.Test.Foundation.Messaging;

/// <summary>
/// Foundation (handler-level) coverage of the <c>is_internal</c> hard visibility boundary — previously only
/// exercised end-to-end (refinement-phase-6 §6.5). The user thread view strips internal notes in the projection;
/// the admin view returns them; a non-staff caller can neither post nor read one.
/// </summary>
public sealed class MessagingInternalBoundaryTests : IDisposable
{
    private static readonly DateTimeOffset Now = new(2026, 8, 10, 10, 0, 0, TimeSpan.Zero);

    private readonly SqliteConnection _connection;
    private readonly ApplicationDbContext _db;
    private readonly UnitOfWork _uow;
    private readonly int _customerUserId;
    private readonly int _adminUserId;
    private readonly long _ticketId;

    public MessagingInternalBoundaryTests()
    {
        _connection = new SqliteConnection("DataSource=:memory:");
        _connection.Open();
        var options = new DbContextOptionsBuilder<ApplicationDbContext>().UseSqlite(_connection).Options;
        _db = new ApplicationDbContext(options, TestFieldEncryptor.Instance);
        _db.Database.EnsureCreated();
        _uow = new UnitOfWork(_db);

        var customer = new User { UserName = "cust1", PhoneNumber = "09120000001", Gender = "male", Name = "علی", FamilyName = "رضایی", IsActive = true };
        var admin = new User { UserName = "admin1", PhoneNumber = "09120000009", Gender = "male", Name = "مدیر", FamilyName = "سیستم", IsActive = true };
        _db.Users.AddRange(customer, admin);
        _db.SaveChanges();
        _customerUserId = customer.Id;
        _adminUserId = admin.Id;

        var ticket = new Ticket { ReferenceCode = "TKT-BND001", Category = TicketCategory.Support, OpenedById = customer.Id };
        _db.Set<Ticket>().Add(ticket);
        _db.SaveChanges();
        _ticketId = ticket.Id;

        _db.Set<TicketParticipant>().Add(new TicketParticipant { TicketId = ticket.Id, UserId = customer.Id, AddedById = customer.Id });
        _db.Set<TicketMessage>().AddRange(
            new TicketMessage { TicketId = ticket.Id, SenderId = customer.Id, Body = "public question", IsInternal = false, SentAt = Now },
            new TicketMessage { TicketId = ticket.Id, SenderId = admin.Id, Body = "internal staff note", IsInternal = true, SentAt = Now.AddMinutes(1) });
        _db.SaveChanges();
    }

    private ICurrentUser AsCustomer() => User(_customerUserId, RoleNames.Customer);
    private ICurrentUser AsAdmin() => User(_adminUserId, RoleNames.Admin);

    private static ICurrentUser User(int id, string role)
    {
        var u = Substitute.For<ICurrentUser>();
        u.UserId.Returns(id);
        u.Roles.Returns(new[] { role });
        return u;
    }

    private IDateTimeProvider Clock()
    {
        var c = Substitute.For<IDateTimeProvider>();
        c.UtcNow.Returns(Now);
        return c;
    }

    private GetTicketThreadQueryHandler ThreadHandler(ICurrentUser user) => new(user, _uow, Clock());
    private PostMessageCommandHandler PostHandler(ICurrentUser user) => new(user, _uow, Clock(), Substitute.For<INotificationDispatcher>());

    [Fact]
    public async Task User_view_strips_internal_messages()
    {
        var result = await ThreadHandler(AsCustomer()).Handle(new GetTicketThreadQuery(_ticketId, AsAdmin: false), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Single(result.Result.Messages);
        Assert.All(result.Result.Messages, m => Assert.False(m.IsInternal));
        Assert.DoesNotContain(result.Result.Messages, m => m.Body == "internal staff note");
    }

    [Fact]
    public async Task Admin_view_includes_internal_messages()
    {
        var result = await ThreadHandler(AsAdmin()).Handle(new GetTicketThreadQuery(_ticketId, AsAdmin: true), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(2, result.Result.Messages.Count);
        Assert.Contains(result.Result.Messages, m => m is { IsInternal: true, Body: "internal staff note" });
    }

    [Fact]
    public async Task Non_staff_admin_view_request_is_forbidden()
    {
        var result = await ThreadHandler(AsCustomer()).Handle(new GetTicketThreadQuery(_ticketId, AsAdmin: true), CancellationToken.None);

        Assert.False(result.IsSuccess);
        Assert.True(result.IsForbidden);
    }

    [Fact]
    public async Task Non_staff_cannot_post_an_internal_note()
    {
        var result = await PostHandler(AsCustomer()).Handle(new PostMessageCommand(_ticketId, "sneaky", IsInternal: true), CancellationToken.None);

        Assert.False(result.IsSuccess);
        Assert.True(result.IsForbidden);
        Assert.DoesNotContain(_db.Set<TicketMessage>().AsNoTracking(), m => m.Body == "sneaky");
    }

    [Fact]
    public async Task Staff_internal_note_is_never_returned_in_the_user_view()
    {
        var post = await PostHandler(AsAdmin()).Handle(new PostMessageCommand(_ticketId, "second internal note", IsInternal: true), CancellationToken.None);
        Assert.True(post.IsSuccess);

        var userView = await ThreadHandler(AsCustomer()).Handle(new GetTicketThreadQuery(_ticketId, AsAdmin: false), CancellationToken.None);
        Assert.True(userView.IsSuccess);
        Assert.All(userView.Result.Messages, m => Assert.False(m.IsInternal));
        Assert.DoesNotContain(userView.Result.Messages, m => m.Body == "second internal note");
    }

    public void Dispose()
    {
        _db.Dispose();
        _connection.Dispose();
    }
}
