using FluentAssertions;
using Microsoft.Extensions.Logging;
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.Caching;
using StorageService.Infrastructure.Storage;
using Xunit;

namespace StorageService.UnitTests.Handlers;

/// <summary>
/// EN: Unit tests for DeleteFileCommandHandler.
/// VI: Unit tests cho DeleteFileCommandHandler.
/// </summary>
public class DeleteFileCommandHandlerTests
{
    private readonly IFileRepository _fileRepository;
    private readonly IQuotaRepository _quotaRepository;
    private readonly IStorageProviderFactory _storageProviderFactory;
    private readonly IStorageProvider _storageProvider;
    private readonly IRedisCacheService _cache;
    private readonly ILogger<DeleteFileCommandHandler> _logger;
    private readonly IUnitOfWork _unitOfWork;
    private readonly DeleteFileCommandHandler _handler;

    private static readonly Guid TestFileId = Guid.NewGuid();
    private const string TestUserId = "user-123";
    private const string TestBucketName = "storage-bucket";
    private const string TestObjectKey = "private/user-123/20260115/abc123_document.pdf";
    private const long TestFileSize = 1024 * 1024; // 1MB

    public DeleteFileCommandHandlerTests()
    {
        _fileRepository = Substitute.For<IFileRepository>();
        _quotaRepository = Substitute.For<IQuotaRepository>();
        _storageProviderFactory = Substitute.For<IStorageProviderFactory>();
        _storageProvider = Substitute.For<IStorageProvider>();
        _cache = Substitute.For<IRedisCacheService>();
        _logger = Substitute.For<ILogger<DeleteFileCommandHandler>>();
        _unitOfWork = Substitute.For<IUnitOfWork>();

        _storageProviderFactory.GetProvider(Arg.Any<StorageProvider>()).Returns(_storageProvider);
        _fileRepository.UnitOfWork.Returns(_unitOfWork);

        _handler = new DeleteFileCommandHandler(
            _fileRepository,
            _quotaRepository,
            _storageProviderFactory,
            _cache,
            _logger);
    }

    #region Happy Path Tests

    [Fact]
    public async Task Handle_FileExists_SoftDeletesFile()
    {
        // Arrange
        var file = CreateRealStorageFile();
        var quota = new UserStorageQuota(TestUserId, maxStorageBytes: TestFileSize * 10);
        quota.AddUsage(TestFileSize);
        var command = new DeleteFileCommand(file.Id, TestUserId);

        _fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
            .Returns(file);
        _quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.DeleteAsync(TestBucketName, Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(true);
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

        // Assert
        result.Success.Should().BeTrue();
        result.Error.Should().BeNull();
        file.IsDeleted.Should().BeTrue();
        _fileRepository.Received(1).Update(file);
    }

    [Fact]
    public async Task Handle_ValidDelete_UpdatesQuota()
    {
        // Arrange
        var file = CreateRealStorageFile();
        var quota = new UserStorageQuota(TestUserId, maxStorageBytes: TestFileSize * 10);
        quota.AddUsage(TestFileSize);
        var initialUsage = quota.UsedStorageBytes;
        var command = new DeleteFileCommand(file.Id, TestUserId);

        _fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
            .Returns(file);
        _quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.DeleteAsync(TestBucketName, Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(true);
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

        // Assert
        quota.UsedStorageBytes.Should().BeLessThan(initialUsage);
        _quotaRepository.Received(1).Update(quota);
    }

    [Fact]
    public async Task Handle_ValidDelete_InvalidatesCache()
    {
        // Arrange
        var file = CreateRealStorageFile();
        var quota = new UserStorageQuota(TestUserId, maxStorageBytes: TestFileSize * 10);
        var command = new DeleteFileCommand(file.Id, TestUserId);

        _fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
            .Returns(file);
        _quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.DeleteAsync(TestBucketName, Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(true);
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

        // Assert
        await _cache.Received().DeleteAsync(
            Arg.Is<string>(key => key.Contains(file.Id.ToString())), 
            Arg.Any<CancellationToken>());
    }

    #endregion

    #region File Not Found Tests

    [Fact]
    public async Task Handle_FileNotFound_ReturnsNotFoundError()
    {
        // Arrange
        var command = new DeleteFileCommand(TestFileId, TestUserId);

        _fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
            .Returns((StorageFile?)null);

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

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

    [Fact]
    public async Task Handle_FileNotFound_DoesNotDeleteFromStorage()
    {
        // Arrange
        var command = new DeleteFileCommand(TestFileId, TestUserId);

        _fileRepository.GetByIdAsync(TestFileId, Arg.Any<CancellationToken>())
            .Returns((StorageFile?)null);

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

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

    #endregion

    #region Permission Tests

    [Fact]
    public async Task Handle_NotOwner_ReturnsPermissionError()
    {
        // Arrange
        var file = new StorageFile(
            "document.pdf",
            TestBucketName,
            TestObjectKey,
            "application/pdf",
            TestFileSize,
            "different-user", // Different owner
            StorageProvider.MinIO);
        var command = new DeleteFileCommand(file.Id, TestUserId);

        _fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
            .Returns(file);

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

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

    [Fact]
    public async Task Handle_NotOwner_DoesNotDeleteFile()
    {
        // Arrange
        var file = new StorageFile(
            "document.pdf",
            TestBucketName,
            TestObjectKey,
            "application/pdf",
            TestFileSize,
            "different-user",
            StorageProvider.MinIO);
        var command = new DeleteFileCommand(file.Id, TestUserId);

        _fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
            .Returns(file);

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

        // Assert
        file.IsDeleted.Should().BeFalse();
        await _storageProvider.DidNotReceive().DeleteAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
    }

    #endregion

    #region Storage Delete Failure Tests

    [Fact]
    public async Task Handle_StorageDeleteFails_StillSoftDeletesFile()
    {
        // Arrange
        var file = CreateRealStorageFile();
        var quota = new UserStorageQuota(TestUserId, maxStorageBytes: TestFileSize * 10);
        var command = new DeleteFileCommand(file.Id, TestUserId);

        _fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
            .Returns(file);
        _quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns(quota);
        _storageProvider.DeleteAsync(TestBucketName, Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(false); // Storage delete fails
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

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

    #endregion

    #region Quota Not Found Tests

    [Fact]
    public async Task Handle_QuotaNotFound_StillDeletesFile()
    {
        // Arrange
        var file = CreateRealStorageFile();
        var command = new DeleteFileCommand(file.Id, TestUserId);

        _fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
            .Returns(file);
        _quotaRepository.GetByUserIdAsync(TestUserId, Arg.Any<CancellationToken>())
            .Returns((UserStorageQuota?)null);
        _storageProvider.DeleteAsync(TestBucketName, Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(true);
        _unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);

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

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

    #endregion

    #region Exception Handling Tests

    [Fact]
    public async Task Handle_RepositoryThrows_ReturnsFailure()
    {
        // Arrange
        var command = new DeleteFileCommand(TestFileId, TestUserId);

        _fileRepository.GetByIdAsync(TestFileId, 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();
    }

    [Fact]
    public async Task Handle_StorageProviderThrows_ReturnsFailure()
    {
        // Arrange
        var file = CreateRealStorageFile();
        var command = new DeleteFileCommand(file.Id, TestUserId);

        _fileRepository.GetByIdAsync(file.Id, Arg.Any<CancellationToken>())
            .Returns(file);
        _storageProvider.DeleteAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new Exception("Storage connection failed"));

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

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

    #endregion

    #region Helper Methods

    private static StorageFile CreateRealStorageFile()
    {
        return new StorageFile(
            "document.pdf",
            TestBucketName,
            TestObjectKey,
            "application/pdf",
            TestFileSize,
            TestUserId,
            StorageProvider.MinIO);
    }

    #endregion
}
