using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using StorageService.API.Application.Commands;
using StorageService.Domain.AggregatesModel.FileAggregate;
using StorageService.Domain.AggregatesModel.QuotaAggregate;
using StorageService.Domain.SeedWork;
using StorageService.Infrastructure.Configuration;
using StorageService.Infrastructure.Storage;
using Xunit;

namespace StorageService.UnitTests.Handlers;

/// <summary>
/// EN: Unit tests for UploadFileCommandHandler.
/// VI: Unit tests cho UploadFileCommandHandler.
/// </summary>
public class UploadFileCommandHandlerTests
{
    private readonly IFileRepository _fileRepository;
    private readonly IQuotaRepository _quotaRepository;
    private readonly IStorageProviderFactory _storageProviderFactory;
    private readonly IStorageProvider _storageProvider;
    private readonly IOptions<StorageSettings> _settings;
    private readonly ILogger<UploadFileCommandHandler> _logger;
    private readonly IUnitOfWork _unitOfWork;
    private readonly UploadFileCommandHandler _handler;

    private const string TestUserId = "user-123";
    private const string TestFileName = "document.pdf";
    private const string TestContentType = "application/pdf";
    private const long TestFileSize = 1024 * 1024; // 1MB
    private const long MaxFileSize = 100 * 1024 * 1024; // 100MB
    private const long MaxQuotaBytes = 1024 * 1024 * 1024; // 1GB

    public UploadFileCommandHandlerTests()
    {
        _fileRepository = Substitute.For<IFileRepository>();
        _quotaRepository = Substitute.For<IQuotaRepository>();
        _storageProviderFactory = Substitute.For<IStorageProviderFactory>();
        _storageProvider = Substitute.For<IStorageProvider>();
        _logger = Substitute.For<ILogger<UploadFileCommandHandler>>();
        _unitOfWork = Substitute.For<IUnitOfWork>();

        _settings = Options.Create(new StorageSettings
        {
            MaxFileSizeBytes = MaxFileSize,
            DefaultBucket = "storage-bucket",
            PreSignedUrlExpirationSeconds = 3600
        });

        _storageProviderFactory.GetProvider().Returns(_storageProvider);
        _storageProvider.ProviderType.Returns(StorageProvider.MinIO);
        _fileRepository.UnitOfWork.Returns(_unitOfWork);

        _handler = new UploadFileCommandHandler(
            _fileRepository,
            _quotaRepository,
            _storageProviderFactory,
            _settings,
            _logger);
    }

    #region Happy Path Tests

    [Fact]
    public async Task Handle_ValidRequest_UploadsFileAndReturnsSuccess()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(StorageResult.Ok("private/user-123/20260115/abc123_document.pdf", TestFileSize, "checksum123"));
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

        // Assert
        result.Success.Should().BeTrue();
        result.FileId.Should().NotBeNull();
        result.ObjectKey.Should().NotBeNullOrEmpty();
        result.Error.Should().BeNull();
    }

    [Fact]
    public async Task Handle_ValidUpload_SavesFileToRepository()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(StorageResult.Ok("objectKey", TestFileSize));
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

        // Assert
        await _fileRepository.Received(1).AddAsync(
            Arg.Is<StorageFile>(f => 
                f.FileName == TestFileName && 
                f.UserId == TestUserId),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_ValidUpload_UpdatesQuota()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(StorageResult.Ok("objectKey", TestFileSize));
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

        // Assert
        // EN: Verify quota usage was added / VI: Xác minh usage đã được thêm
        quota.UsedStorageBytes.Should().Be(TestFileSize);
        _quotaRepository.Received(1).Update(quota);
    }

    [Fact]
    public async Task Handle_ValidUpload_CallsSaveEntities()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(StorageResult.Ok("objectKey", TestFileSize));
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

        // Assert
        await _unitOfWork.Received(1).SaveEntitiesAsync(Arg.Any<CancellationToken>());
    }

    #endregion

    #region File Size Validation Tests

    [Fact]
    public async Task Handle_ExceedsMaxSize_ReturnsFailure()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[100]);
        var command = new UploadFileCommand(
            stream,
            TestFileName,
            TestContentType,
            MaxFileSize + 1,
            TestUserId);

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

        // Assert
        result.Success.Should().BeFalse();
        result.Error.Should().Contain("exceeds maximum");
        result.FileId.Should().BeNull();
    }

    [Fact]
    public async Task Handle_ExceedsMaxSize_DoesNotUpload()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[100]);
        var command = new UploadFileCommand(
            stream,
            TestFileName,
            TestContentType,
            MaxFileSize + 1,
            TestUserId);

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

        // Assert
        await _storageProvider.DidNotReceive().UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>());
    }

    #endregion

    #region Quota Validation Tests

    [Fact]
    public async Task Handle_QuotaExceeded_ReturnsFailure()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        // Create quota with no space
        var quota = new UserStorageQuota(TestUserId, maxStorageBytes: TestFileSize - 1);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);

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

        // Assert
        result.Success.Should().BeFalse();
        result.Error.Should().Contain("Quota");
    }

    [Fact]
    public async Task Handle_QuotaExceeded_DoesNotUpload()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        var quota = new UserStorageQuota(TestUserId, maxStorageBytes: TestFileSize - 1);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);

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

        // Assert
        await _storageProvider.DidNotReceive().UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>());
    }

    #endregion

    #region Storage Provider Tests

    [Fact]
    public async Task Handle_StorageUploadFails_ReturnsFailure()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(StorageResult.Fail("Storage connectivity issue"));

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

        // Assert
        result.Success.Should().BeFalse();
        result.Error.Should().Be("Storage connectivity issue");
    }

    [Fact]
    public async Task Handle_StorageUploadFails_DoesNotSaveFile()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(StorageResult.Fail("Failed"));

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

        // Assert
        await _fileRepository.DidNotReceive().AddAsync(
            Arg.Any<StorageFile>(), Arg.Any<CancellationToken>());
    }

    #endregion

    #region Access Level Tests

    [Theory]
    [InlineData(FileAccessLevel.Public, "public/")]
    [InlineData(FileAccessLevel.Private, "private/")]
    [InlineData(FileAccessLevel.Shared, "shared/")]
    public async Task Handle_AccessLevel_GeneratesCorrectPrefix(FileAccessLevel accessLevel, string expectedPrefix)
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = new UploadFileCommand(
            stream,
            TestFileName,
            TestContentType,
            TestFileSize,
            TestUserId,
            AccessLevel: accessLevel);

        var quota = CreateQuotaWithSpace(MaxQuotaBytes);
        string? capturedObjectKey = null;

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.UploadAsync(
            Arg.Any<string>(), 
            Arg.Do<string>(key => capturedObjectKey = key), 
            Arg.Any<Stream>(), 
            Arg.Any<string>(), 
            Arg.Any<CancellationToken>())
            .Returns(StorageResult.Ok("objectKey", TestFileSize));
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

        // Assert
        capturedObjectKey.Should().StartWith(expectedPrefix);
    }

    #endregion

    #region Exception Handling Tests

    [Fact]
    public async Task Handle_RepositoryThrows_ReturnsFailure()
    {
        // Arrange
        using var stream = new MemoryStream(new byte[TestFileSize]);
        var command = CreateValidCommand(stream);
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.UploadAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<Stream>(), 
            Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(StorageResult.Ok("objectKey", TestFileSize));
        _fileRepository.AddAsync(Arg.Any<StorageFile>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new Exception("Database error"));

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

        // Assert
        result.Success.Should().BeFalse();
        result.Error.Should().NotBeNullOrEmpty();
    }

    #endregion

    #region Helper Methods

    private static UploadFileCommand CreateValidCommand(Stream stream)
    {
        return new UploadFileCommand(
            stream,
            TestFileName,
            TestContentType,
            TestFileSize,
            TestUserId);
    }

    private static UserStorageQuota CreateQuotaWithSpace(long maxStorage)
    {
        return new UserStorageQuota(TestUserId, maxStorageBytes: maxStorage);
    }

    #endregion
}
