using Microsoft.Extensions.Options;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Storage;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Media;
using TeleWave.Infrastructure.Media;
using Xunit;

namespace TeleWave.Application.Tests.Storage;

/// <summary>
/// Отчёт по диску. Сегменты программ и заставок лежат в одном каталоге, и разделить их можно
/// только по базе — это и проверяется в первую очередь.
/// </summary>
public class StorageStatsTests : IDisposable
{
    private readonly string _root = Path.Combine(
        Path.GetTempPath(),
        "telewave-storage-" + Guid.NewGuid().ToString("N")
    );

    private FileSystemStorageInspector NewInspector()
    {
        var options = Options.Create(new StorageOptions { RootPath = _root });
        return new FileSystemStorageInspector(new MediaPathResolver(options), options);
    }

    private void Write(string relativePath, int bytes)
    {
        var path = Path.Combine(_root, relativePath);
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        File.WriteAllBytes(path, new byte[bytes]);
    }

    private static readonly IReadOnlySet<Guid> Empty = new HashSet<Guid>();

    private static long BytesOf(StorageUsage usage, StorageArea area) =>
        usage.Areas.Single(a => a.Area == area).Bytes;

    [Fact]
    public async Task Inspect_SplitsProgramAndBumperSegments()
    {
        var program = Guid.NewGuid();
        var bumper = Guid.NewGuid();
        Write($"assets/{program:N}/seg00000.ts", 300);
        Write($"assets/{bumper:N}/seg00000.ts", 100);
        Write("images/poster.png", 50);
        Write("uploads/half-uploaded.mkv", 20);
        Write("originals/movie.mkv", 10);

        var usage = await NewInspector()
            .InspectAsync(new HashSet<Guid> { bumper }, refresh: false, CancellationToken.None);

        Assert.Equal(300, BytesOf(usage, StorageArea.Assets));
        Assert.Equal(100, BytesOf(usage, StorageArea.BumperAssets));
        Assert.Equal(50, BytesOf(usage, StorageArea.Images));
        Assert.Equal(20, BytesOf(usage, StorageArea.Uploads));
        Assert.Equal(10, BytesOf(usage, StorageArea.Originals));
        Assert.Equal(480, usage.StorageBytes);
        Assert.Equal(5, usage.StorageFiles);
        Assert.Equal(_root, usage.RootPath);
    }

    [Fact]
    public async Task Inspect_CountsStrayFilesAsOther()
    {
        Write("забытая-папка/dump.sql", 70);
        Write("readme.txt", 30);

        var usage = await NewInspector()
            .InspectAsync(Empty, refresh: false, CancellationToken.None);

        // «Прочее» обычно ноль — и именно поэтому его надо показывать, когда оно не ноль.
        Assert.Equal(100, BytesOf(usage, StorageArea.Other));
    }

    [Fact]
    public async Task Inspect_ReusesCache_UntilRefreshAsked()
    {
        Write("images/a.png", 10);
        var inspector = NewInspector();

        var first = await inspector.InspectAsync(Empty, refresh: false, CancellationToken.None);
        Write("images/b.png", 40);
        var cached = await inspector.InspectAsync(Empty, refresh: false, CancellationToken.None);
        var fresh = await inspector.InspectAsync(Empty, refresh: true, CancellationToken.None);

        // Обход дорогой: без явной просьбы отдаём прежнюю цифру вместе с её временем.
        Assert.Equal(first.ComputedAt, cached.ComputedAt);
        Assert.Equal(10, BytesOf(cached, StorageArea.Images));
        Assert.Equal(50, BytesOf(fresh, StorageArea.Images));
    }

    [Fact]
    public async Task Handler_PassesGeneratedAssetsToInspector()
    {
        var fixture = new TestDb();
        var generated = MediaAsset.RegisterGenerated("Заставка");
        var uploaded = MediaAsset.Register("film.mkv", ".mkv", MediaSource.Upload);
        await using (var seed = fixture.New())
        {
            seed.MediaAssets.AddRange(generated, uploaded);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var inspector = new CapturingInspector();
        await using var db = fixture.New();
        var result = await new GetStorageStatsQueryHandler(db, inspector).Handle(
            new GetStorageStatsQuery(Refresh: true),
            CancellationToken.None
        );

        Assert.True(inspector.Refresh);
        Assert.Equal([generated.Id], inspector.GeneratedAssetIds);
        Assert.Equal("/media", result.RootPath);
    }

    private sealed class CapturingInspector : IStorageInspector
    {
        public IReadOnlySet<Guid> GeneratedAssetIds { get; private set; } = new HashSet<Guid>();
        public bool Refresh { get; private set; }

        public Task<StorageUsage> InspectAsync(
            IReadOnlySet<Guid> generatedAssetIds,
            bool refresh,
            CancellationToken cancellationToken
        )
        {
            GeneratedAssetIds = generatedAssetIds;
            Refresh = refresh;
            return Task.FromResult(
                new StorageUsage("/media", 0, 0, 0, 0, 0, [], DateTimeOffset.UnixEpoch)
            );
        }
    }

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