using GmRelay.Web.Services.Portfolio.Covers;

namespace GmRelay.Bot.Tests.Web;

public sealed class LocalPortfolioCoverStorageTests : IDisposable
{
    private readonly string _storagePath;
    private readonly LocalPortfolioCoverStorage _storage;

    public LocalPortfolioCoverStorageTests()
    {
        _storagePath = Path.Combine(
            Path.GetTempPath(),
            "gmrelay-portfolio-covers-tests-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(_storagePath);
        _storage = new LocalPortfolioCoverStorage(new PortfolioCoverStorageOptions
        {
            StoragePath = _storagePath
        });
    }

    public void Dispose()
    {
        if (Directory.Exists(_storagePath))
        {
            Directory.Delete(_storagePath, recursive: true);
        }
        GC.SuppressFinalize(this);
    }

    [Fact]
    public async Task SaveAsync_ShouldPersistPngWithRandomProviderNeutralKey()
    {
        await using var stream = new MemoryStream(
            [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00]);

        var result = await _storage.SaveAsync(stream, "image/png");

        Assert.EndsWith(".png", result.StorageKey, StringComparison.Ordinal);
        Assert.StartsWith("/portfolio-covers/", _storage.GetPublicPath(result.StorageKey), StringComparison.Ordinal);
        Assert.True(File.Exists(Path.Combine(_storagePath, result.StorageKey)));
    }

    [Theory]
    [InlineData("image/jpeg")]
    [InlineData("image/png")]
    [InlineData("image/webp")]
    public async Task SaveAsync_ShouldRejectMismatchedSignature(string contentType)
    {
        await using var stream = new MemoryStream([0x00, 0x01, 0x02, 0x03]);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _storage.SaveAsync(stream, contentType));
    }

    [Fact]
    public async Task SaveAsync_ShouldPersistJpegWithCorrectSignature()
    {
        await using var stream = new MemoryStream(
            [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46]);

        var result = await _storage.SaveAsync(stream, "image/jpeg");

        Assert.EndsWith(".jpg", result.StorageKey, StringComparison.Ordinal);
        Assert.Equal("image/jpeg", result.ContentType);
        Assert.True(File.Exists(Path.Combine(_storagePath, result.StorageKey)));
    }

    [Fact]
    public async Task SaveAsync_ShouldPersistWebpWithRiffWebpSignature()
    {
        await using var stream = new MemoryStream(
            [0x52, 0x49, 0x46, 0x46, 0x1A, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38]);

        var result = await _storage.SaveAsync(stream, "image/webp");

        Assert.EndsWith(".webp", result.StorageKey, StringComparison.Ordinal);
        Assert.Equal("image/webp", result.ContentType);
        Assert.True(File.Exists(Path.Combine(_storagePath, result.StorageKey)));
    }

    [Fact]
    public async Task SaveAsync_ShouldRejectStreamLargerThanMaxBytes()
    {
        var oversized = new byte[LocalPortfolioCoverStorage.MaxBytes + 1];
        await using var stream = new MemoryStream(oversized);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _storage.SaveAsync(stream, "image/png"));
    }

    [Fact]
    public async Task SaveAsync_ShouldRejectUnknownContentType()
    {
        await using var stream = new MemoryStream([0x89, 0x50, 0x4E, 0x47]);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _storage.SaveAsync(stream, "application/octet-stream"));
    }

    [Fact]
    public async Task DeleteIfExistsAsync_ShouldRemoveExistingKey()
    {
        await using var stream = new MemoryStream(
            [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
        var result = await _storage.SaveAsync(stream, "image/png");
        var path = Path.Combine(_storagePath, result.StorageKey);
        Assert.True(File.Exists(path));

        await _storage.DeleteIfExistsAsync(result.StorageKey);

        Assert.False(File.Exists(path));
    }

    [Fact]
    public async Task DeleteIfExistsAsync_ShouldBeNoOpForMissingKey()
    {
        var key = Guid.NewGuid().ToString("N") + ".png";

        await _storage.DeleteIfExistsAsync(key);
    }

    [Fact]
    public async Task DeleteIfExistsAsync_ShouldRejectPathTraversal()
    {
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _storage.DeleteIfExistsAsync("../escape.png"));
    }

    [Fact]
    public async Task DeleteIfExistsAsync_ShouldRejectKeyWithInvalidExtension()
    {
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _storage.DeleteIfExistsAsync(Guid.NewGuid().ToString("N") + ".gif"));
    }

    [Fact]
    public void GetPublicPath_ShouldEscapeSpecialCharacters()
    {
        var key = "0123456789abcdef0123456789abcdef" + ".png";

        var path = _storage.GetPublicPath(key);

        Assert.Equal("/portfolio-covers/" + Uri.EscapeDataString(key), path);
    }

    [Fact]
    public async Task SaveAsync_ShouldGenerateUniqueKeys()
    {
        await using var stream1 = new MemoryStream(
            [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
        await using var stream2 = new MemoryStream(
            [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);

        var first = await _storage.SaveAsync(stream1, "image/png");
        var second = await _storage.SaveAsync(stream2, "image/png");

        Assert.NotEqual(first.StorageKey, second.StorageKey);
    }

    [Fact]
    public async Task SaveAsync_ShouldNotLeaveTempFileBehind()
    {
        await using var stream = new MemoryStream(
            [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);

        await _storage.SaveAsync(stream, "image/png");

        var tempFiles = Directory.GetFiles(_storagePath, "*.tmp");
        Assert.Empty(tempFiles);
    }
}
