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.Infrastructure.Configuration;
using StorageService.Infrastructure.Storage;
using Xunit;

namespace StorageService.UnitTests.Handlers;

/// <summary>
/// EN: Unit tests for SignUploadCommandHandler.
/// VI: Unit tests cho SignUploadCommandHandler.
/// </summary>
public class SignUploadCommandHandlerTests
{
    private readonly IQuotaRepository _quotaRepository;
    private readonly IStorageProviderFactory _storageProviderFactory;
    private readonly IStorageProvider _storageProvider;
    private readonly IOptions<StorageSettings> _settings;
    private readonly ILogger<SignUploadCommandHandler> _logger;
    private readonly SignUploadCommandHandler _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 SignUploadCommandHandlerTests()
    {
        _quotaRepository = Substitute.For<IQuotaRepository>();
        _storageProviderFactory = Substitute.For<IStorageProviderFactory>();
        _storageProvider = Substitute.For<IStorageProvider>();
        _logger = Substitute.For<ILogger<SignUploadCommandHandler>>();

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

        _storageProviderFactory.GetProvider().Returns(_storageProvider);

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

    #region Happy Path Tests

    [Fact]
    public async Task Handle_ValidRequest_ReturnsSuccessWithPresignedUrl()
    {
        // Arrange
        var command = CreateValidCommand();
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);
        var expectedUrl = "https://minio.example.com/bucket/object?X-Amz-Signature=...";

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(expectedUrl);

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

        // Assert
        result.Success.Should().BeTrue();
        result.UploadUrl.Should().Be(expectedUrl);
        result.ObjectKey.Should().NotBeNullOrEmpty();
        result.ExpiresAt.Should().BeAfter(DateTime.UtcNow);
        result.Error.Should().BeNull();
    }

    [Fact]
    public async Task Handle_ValidRequest_EnsuresBucketExists()
    {
        // Arrange
        var command = CreateValidCommand();
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns("https://example.com/upload");

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

        // Assert
        await _storageProvider.Received(1).EnsureBucketExistsAsync(
            "storage-bucket", Arg.Any<CancellationToken>());
    }

    #endregion

    #region File Size Validation Tests

    [Fact]
    public async Task Handle_ExceedsMaxFileSize_ReturnsFailure()
    {
        // Arrange
        var command = new SignUploadCommand(
            TestUserId,
            TestFileName,
            TestContentType,
            MaxFileSize + 1);

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

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

    [Fact]
    public async Task Handle_FileSizeAtLimit_ReturnsSuccess()
    {
        // Arrange
        var command = new SignUploadCommand(
            TestUserId,
            TestFileName,
            TestContentType,
            MaxFileSize);

        var quota = CreateQuotaWithSpace(MaxQuotaBytes);
        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns("https://example.com/upload");

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

        // Assert
        result.Success.Should().BeTrue();
    }

    #endregion

    #region Quota Validation Tests

    [Fact]
    public async Task Handle_QuotaExceeded_ReturnsFailure()
    {
        // Arrange
        var command = CreateValidCommand();
        // Create quota with no space left
        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");
        result.UploadUrl.Should().BeNull();
    }

    [Fact]
    public async Task Handle_ValidQuota_ChecksQuotaRepository()
    {
        // Arrange
        var command = CreateValidCommand();
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns("https://example.com/upload");

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

        // Assert
        await _quotaRepository.Received(1).GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>());
    }

    #endregion

    #region Object Key Generation Tests

    [Fact]
    public async Task Handle_PublicAccessLevel_GeneratesPublicPrefix()
    {
        // Arrange
        var command = new SignUploadCommand(
            TestUserId,
            TestFileName,
            TestContentType,
            TestFileSize,
            FileAccessLevel.Public);

        var quota = CreateQuotaWithSpace(MaxQuotaBytes);
        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns("https://example.com/upload");

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

        // Assert
        result.ObjectKey.Should().StartWith("public/");
    }

    [Fact]
    public async Task Handle_PrivateAccessLevel_GeneratesPrivatePrefix()
    {
        // Arrange
        var command = new SignUploadCommand(
            TestUserId,
            TestFileName,
            TestContentType,
            TestFileSize,
            FileAccessLevel.Private);

        var quota = CreateQuotaWithSpace(MaxQuotaBytes);
        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns("https://example.com/upload");

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

        // Assert
        result.ObjectKey.Should().StartWith("private/");
    }

    [Fact]
    public async Task Handle_SharedAccessLevel_GeneratesSharedPrefix()
    {
        // Arrange
        var command = new SignUploadCommand(
            TestUserId,
            TestFileName,
            TestContentType,
            TestFileSize,
            FileAccessLevel.Shared);

        var quota = CreateQuotaWithSpace(MaxQuotaBytes);
        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns("https://example.com/upload");

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

        // Assert
        result.ObjectKey.Should().StartWith("shared/");
    }

    [Fact]
    public async Task Handle_ValidRequest_ObjectKeyContainsUserId()
    {
        // Arrange
        var command = CreateValidCommand();
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns("https://example.com/upload");

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

        // Assert
        result.ObjectKey.Should().Contain(TestUserId);
    }

    [Fact]
    public async Task Handle_ValidRequest_ObjectKeyContainsFileName()
    {
        // Arrange
        var command = CreateValidCommand();
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns("https://example.com/upload");

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

        // Assert
        result.ObjectKey.Should().EndWith(".pdf");
        result.ObjectKey.Should().Contain("document");
    }

    #endregion

    #region Exception Handling Tests

    [Fact]
    public async Task Handle_StorageProviderThrows_ReturnsFailure()
    {
        // Arrange
        var command = CreateValidCommand();
        var quota = CreateQuotaWithSpace(MaxQuotaBytes);

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.EnsureBucketExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        _storageProvider.GetPreSignedUploadUrlAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new Exception("Storage connection failed"));

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

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

    [Fact]
    public async Task Handle_QuotaRepositoryThrows_ReturnsFailure()
    {
        // Arrange
        var command = CreateValidCommand();

        _quotaRepository.GetOrCreateAsync(TestUserId, Arg.Any<CancellationToken>())
            .ThrowsAsync(new Exception("Database connection failed"));

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

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

    #endregion

    #region Helper Methods

    private static SignUploadCommand CreateValidCommand()
    {
        return new SignUploadCommand(
            TestUserId,
            TestFileName,
            TestContentType,
            TestFileSize);
    }

    /// <summary>
    /// EN: Create a real UserStorageQuota with available space.
    /// VI: Tạo real UserStorageQuota với dung lượng khả dụng.
    /// </summary>
    private static UserStorageQuota CreateQuotaWithSpace(long maxStorage)
    {
        return new UserStorageQuota(TestUserId, maxStorageBytes: maxStorage);
    }

    #endregion
}
