using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using TeleWave.Infrastructure.Media;
using Xunit;

namespace TeleWave.Application.Tests.Media;

/// <summary>
/// Уборка перевалочного каталога: в <c>uploads/</c> не должно оставаться файлов, на которые никто
/// не сослался — это гигабайты на диске, невидимые в админке.
/// </summary>
public class UploadsCleanupTests : IDisposable
{
    private readonly string _root = Path.Combine(
        Path.GetTempPath(),
        "telewave-tests",
        Guid.NewGuid().ToString("N")
    );

    private MediaPathResolver Paths() =>
        new(Options.Create(new StorageOptions { RootPath = _root }));

    [Fact]
    public async Task DeletesPartialUpload_WhenStreamFails()
    {
        var paths = Paths();
        paths.EnsureDirectories();
        var storage = new FileSystemMediaStorage(paths);

        await Assert.ThrowsAsync<IOException>(() =>
            storage.SaveUploadAsync(new FailingStream(), ".mkv", CancellationToken.None)
        );

        Assert.Empty(Directory.GetFiles(paths.UploadsDir));
    }

    [Fact]
    public void Sweep_DeletesStaleFilesOnly()
    {
        var paths = Paths();
        paths.EnsureDirectories();
        var stale = Path.Combine(paths.UploadsDir, "stale.mkv");
        var fresh = Path.Combine(paths.UploadsDir, "fresh.mkv");
        // Идущую заливку от брошенного файла отличает именно время последней записи.
        WriteAged(stale, TimeSpan.FromHours(25));
        WriteAged(fresh, TimeSpan.Zero);

        UploadsCleaner.Sweep(paths, TimeSpan.FromHours(24), NullLogger.Instance);

        Assert.False(File.Exists(stale));
        Assert.True(File.Exists(fresh));
    }

    [Fact]
    public void Sweep_SurvivesMissingDirectory()
    {
        // Каталога может не быть на самом первом старте — это не повод падать. И заводить его
        // уборка тоже не должна: каталоги создаёт хранилище, когда в них появляется нужда.
        var paths = Paths();

        UploadsCleaner.Sweep(paths, TimeSpan.FromHours(24), NullLogger.Instance);

        Assert.False(Directory.Exists(paths.UploadsDir));
    }

    /// <summary>
    /// Кладёт файл и «состаривает» его. Отметку перепроверяем: Windows дописывает время последней
    /// записи при закрытии handle и может затереть выставленное сразу после записи.
    /// </summary>
    private static void WriteAged(string path, TimeSpan age)
    {
        File.WriteAllText(path, "x");
        if (age == TimeSpan.Zero)
            return;

        var stamp = DateTime.UtcNow - age;
        for (var attempt = 0; attempt < 20; attempt++)
        {
            File.SetLastWriteTimeUtc(path, stamp);
            if ((File.GetLastWriteTimeUtc(path) - stamp).Duration() < TimeSpan.FromSeconds(1))
                return;
            Thread.Sleep(20);
        }

        Assert.Fail("Не удалось выставить файлу время последней записи.");
    }

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

    /// <summary>Поток, обрывающийся на середине, — имитация отвалившегося клиента.</summary>
    private sealed class FailingStream : Stream
    {
        public override bool CanRead => true;
        public override bool CanSeek => false;
        public override bool CanWrite => false;
        public override long Length => throw new NotSupportedException();
        public override long Position
        {
            get => throw new NotSupportedException();
            set => throw new NotSupportedException();
        }

        public override int Read(byte[] buffer, int offset, int count) =>
            throw new IOException("Соединение разорвано.");

        public override ValueTask<int> ReadAsync(
            Memory<byte> buffer,
            CancellationToken cancellationToken = default
        ) => throw new IOException("Соединение разорвано.");

        public override void Flush() { }

        public override long Seek(long offset, SeekOrigin origin) =>
            throw new NotSupportedException();

        public override void SetLength(long value) => throw new NotSupportedException();

        public override void Write(byte[] buffer, int offset, int count) =>
            throw new NotSupportedException();
    }
}
