using FluentAssertions;
using MembershipService.API.Application.Commands;
using MembershipService.Domain.AggregatesModel.ExperienceAggregate;
using MembershipService.Domain.AggregatesModel.LevelAggregate;
using MembershipService.Domain.AggregatesModel.MemberAggregate;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;

namespace MembershipService.UnitTests.Handlers;

/// <summary>
/// EN: Unit tests for AddExperienceCommandHandler.
/// VI: Unit tests cho AddExperienceCommandHandler.
/// </summary>
public class AddExperienceCommandHandlerTests
{
    private readonly IMemberRepository _memberRepository;
    private readonly ILevelDefinitionRepository _levelDefinitionRepository;
    private readonly IExperienceTransactionRepository _experienceTransactionRepository;
    private readonly ILogger<AddExperienceCommandHandler> _logger;
    private readonly AddExperienceCommandHandler _handler;

    public AddExperienceCommandHandlerTests()
    {
        _memberRepository = Substitute.For<IMemberRepository>();
        _levelDefinitionRepository = Substitute.For<ILevelDefinitionRepository>();
        _experienceTransactionRepository = Substitute.For<IExperienceTransactionRepository>();
        _logger = Substitute.For<ILogger<AddExperienceCommandHandler>>();

        _handler = new AddExperienceCommandHandler(
            _memberRepository,
            _levelDefinitionRepository,
            _experienceTransactionRepository,
            _logger);
    }

    [Fact]
    public async Task Handle_ValidCommand_ShouldAddExperienceAndReturnResult()
    {
        // Arrange
        var memberId = Guid.NewGuid();
        var member = new Member(memberId);
        var levelRules = CreateDefaultLevelRules();

        var command = new AddExperienceCommand
        {
            MemberId = memberId,
            Points = 50,
            SourceId = ExperienceSource.Purchase.Id,
            ReferenceId = "ORDER-123"
        };

        _memberRepository.GetByIdAsync(memberId, Arg.Any<CancellationToken>())
            .Returns(member);
        _levelDefinitionRepository.GetAllActiveAsync()
            .Returns(levelRules);
        _memberRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
            .Returns(true);

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.MemberId.Should().Be(memberId);
        result.PointsAdded.Should().Be(50);
        result.CurrentExp.Should().Be(50);
        result.LeveledUp.Should().BeFalse();

        // Verify interactions
        await _memberRepository.Received(1).GetByIdAsync(memberId, Arg.Any<CancellationToken>());
        await _levelDefinitionRepository.Received(1).GetAllActiveAsync();
        _experienceTransactionRepository.Received(1).Add(Arg.Any<ExperienceTransaction>());
        _memberRepository.Received(1).Update(member);
    }

    [Fact]
    public async Task Handle_EnoughExpToLevelUp_ShouldReturnLeveledUpTrue()
    {
        // Arrange
        var memberId = Guid.NewGuid();
        var member = new Member(memberId);
        var levelRules = CreateDefaultLevelRules();

        var command = new AddExperienceCommand
        {
            MemberId = memberId,
            Points = 150, // Enough for Silver (100 EXP)
            SourceId = ExperienceSource.Purchase.Id
        };

        _memberRepository.GetByIdAsync(memberId, Arg.Any<CancellationToken>())
            .Returns(member);
        _levelDefinitionRepository.GetAllActiveAsync()
            .Returns(levelRules);
        _memberRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
            .Returns(true);

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.LeveledUp.Should().BeTrue();
        result.PreviousLevel.Should().Be(1);
        result.CurrentLevel.Should().Be(2);
    }

    [Fact]
    public async Task Handle_MemberNotFound_ShouldThrowKeyNotFoundException()
    {
        // Arrange
        var memberId = Guid.NewGuid();
        var command = new AddExperienceCommand
        {
            MemberId = memberId,
            Points = 50,
            SourceId = ExperienceSource.Purchase.Id
        };

        _memberRepository.GetByIdAsync(memberId, Arg.Any<CancellationToken>())
            .Returns((Member?)null);

        // Act & Assert
        await Assert.ThrowsAsync<KeyNotFoundException>(() =>
            _handler.Handle(command, CancellationToken.None));
    }

    [Fact]
    public async Task Handle_NoActiveLevelRules_ShouldThrowInvalidOperationException()
    {
        // Arrange
        var memberId = Guid.NewGuid();
        var member = new Member(memberId);
        var command = new AddExperienceCommand
        {
            MemberId = memberId,
            Points = 50,
            SourceId = ExperienceSource.Purchase.Id
        };

        _memberRepository.GetByIdAsync(memberId, Arg.Any<CancellationToken>())
            .Returns(member);
        _levelDefinitionRepository.GetAllActiveAsync()
            .Returns(new List<LevelDefinition>());

        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            _handler.Handle(command, CancellationToken.None));
    }

    [Fact]
    public async Task Handle_MultipleSourceTypes_ShouldHandleAllSourcesCorrectly()
    {
        // Arrange
        var memberId = Guid.NewGuid();
        var member = new Member(memberId);
        var levelRules = CreateDefaultLevelRules();

        _memberRepository.GetByIdAsync(memberId, Arg.Any<CancellationToken>())
            .Returns(member);
        _levelDefinitionRepository.GetAllActiveAsync()
            .Returns(levelRules);
        _memberRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
            .Returns(true);

        foreach (var sourceId in new[] { 1, 2, 3, 4, 5, 6, 7 })
        {
            // Reset member for each test
            member = new Member(Guid.NewGuid());
            _memberRepository.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
                .Returns(member);

            var command = new AddExperienceCommand
            {
                MemberId = member.Id,
                Points = 10,
                SourceId = sourceId
            };

            // Act
            var result = await _handler.Handle(command, CancellationToken.None);

            // Assert
            result.PointsAdded.Should().Be(10);
        }
    }

    [Fact]
    public async Task Handle_WithMetadata_ShouldPassMetadataToTransaction()
    {
        // Arrange
        var memberId = Guid.NewGuid();
        var member = new Member(memberId);
        var levelRules = CreateDefaultLevelRules();
        var metadata = "{\"items\": 5, \"totalAmount\": 100}";

        var command = new AddExperienceCommand
        {
            MemberId = memberId,
            Points = 50,
            SourceId = ExperienceSource.Purchase.Id,
            ReferenceId = "ORDER-123",
            Metadata = metadata
        };

        _memberRepository.GetByIdAsync(memberId, Arg.Any<CancellationToken>())
            .Returns(member);
        _levelDefinitionRepository.GetAllActiveAsync()
            .Returns(levelRules);
        _memberRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
            .Returns(true);

        ExperienceTransaction? capturedTransaction = null;
        _experienceTransactionRepository.Add(Arg.Do<ExperienceTransaction>(t => capturedTransaction = t));

        // Act
        await _handler.Handle(command, CancellationToken.None);

        // Assert - Verify transaction was captured with metadata
        // Note: The actual transaction metadata is set in Member.AddExperience
        _experienceTransactionRepository.Received(1).Add(Arg.Any<ExperienceTransaction>());
    }

    private static IReadOnlyList<LevelDefinition> CreateDefaultLevelRules()
    {
        return new List<LevelDefinition>
        {
            new(1, "Bronze", 0, "Starting level"),
            new(2, "Silver", 100, "Reach 100 EXP"),
            new(3, "Gold", 300, "Reach 300 EXP"),
            new(4, "Platinum", 600, "Reach 600 EXP"),
            new(5, "Diamond", 1000, "Reach 1000 EXP")
        };
    }
}
