using System.IO;
using System.IO.Compression;
using Xunit;
using ytLive.Services.Encoder;

namespace ytLive.Tests;

/// <summary>
/// The FFmpeg locator (TASK 4 ship step 2): resolves ffmpeg.exe by probing PATH
/// first, then the cache in the tools directory, then pulling the pinned BtbN
/// LGPL-shared zip. The integration test drives the full ladder against a temp
/// tools dir and a fake downloader that returns a real in-memory zip; the units
/// pin down the failure and edge cases. No network, no real binary.
/// </summary>
public class FfmpegLocatorTests
{
    private static byte[] MakeZip(
        string exePath = "ffmpeg-master-latest-win64-lgpl-shared/bin/ffmpeg.exe",
        string[]? dllPaths = null)
    {
        using var ms = new MemoryStream();
        using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
        {
            var entry = archive.CreateEntry(exePath);
            using (var writer = new StreamWriter(entry.Open()))
                writer.Write("dummy ffmpeg binary");
            foreach (var dll in dllPaths ?? Array.Empty<string>())
            {
                var dllEntry = archive.CreateEntry(dll);
                using var dllWriter = new StreamWriter(dllEntry.Open());
                dllWriter.Write("dummy dll");
            }
        }
        return ms.ToArray();
    }

    private static string TempDir()
    {
        var dir = Path.Combine(Path.GetTempPath(), "ytllive-tests-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(dir);
        return dir;
    }

    private sealed class RecordingDownloader
    {
        public int Calls { get; private set; }
        public byte[] Payload { get; set; } = MakeZip();
        public Exception? Error { get; set; }

        public Task<byte[]> DownloadAsync(string url, CancellationToken ct)
        {
            Calls++;
            if (Error != null) throw Error;
            return Task.FromResult(Payload);
        }
    }

    [Fact]
    public async Task Locate_Integration_FullDecisionLadder()
    {
        var toolsDir = TempDir();
        try
        {
            // 1. PATH hit wins, downloader never invoked.
            var pathDir = TempDir();
            var pathExe = Path.Combine(pathDir, FfmpegLocator.FileName);
            File.WriteAllText(pathExe, "user's ffmpeg");
            var downloader = new RecordingDownloader();

            var fromPath = new FfmpegLocator([pathDir], toolsDir, downloader.DownloadAsync);
            Assert.Equal(pathExe, await fromPath.LocateAsync());
            Assert.Equal(0, downloader.Calls);

            // 2. Cache hit skips the network.
            var cached = Path.Combine(toolsDir, FfmpegLocator.FileName);
            Directory.CreateDirectory(toolsDir);
            File.WriteAllText(cached, "cached ffmpeg");
            var fromCache = new FfmpegLocator([], toolsDir, downloader.DownloadAsync);
            Assert.Equal(cached, await fromCache.LocateAsync());
            Assert.Equal(0, downloader.Calls);

            // 3. Cold cache downloads exactly once, extracts ffmpeg.exe, and the
            //    second call serves the cache without re-downloading.
            File.Delete(cached);
            var coldTools = TempDir();
            var cold = new FfmpegLocator([], coldTools, downloader.DownloadAsync);
            var resolved = await cold.LocateAsync();
            Assert.Equal(Path.Combine(coldTools, FfmpegLocator.FileName), resolved);
            Assert.Equal(1, downloader.Calls);
            Assert.True(new FileInfo(resolved).Length > 0);
            Assert.Equal(resolved, await cold.LocateAsync());
            Assert.Equal(1, downloader.Calls);
        }
        finally
        {
            Directory.Delete(toolsDir, recursive: true);
        }
    }

    [Fact]
    public async Task Locate_ZeroByteCache_IsRefreshed()
    {
        var toolsDir = TempDir();
        try
        {
            File.WriteAllText(Path.Combine(toolsDir, FfmpegLocator.FileName), "");
            var downloader = new RecordingDownloader();
            var locator = new FfmpegLocator([], toolsDir, downloader.DownloadAsync);
            var resolved = await locator.LocateAsync();
            Assert.Equal(1, downloader.Calls);
            Assert.True(new FileInfo(resolved).Length > 0);
        }
        finally
        {
            Directory.Delete(toolsDir, recursive: true);
        }
    }

    [Fact]
    public async Task Locate_SharedBuild_ExtractsDllsAlongsideExe()
    {
        var toolsDir = TempDir();
        try
        {
            var downloader = new RecordingDownloader
            {
                Payload = MakeZip(dllPaths:
                [
                    "ffmpeg-master-latest-win64-lgpl-shared/bin/avcodec-61.dll",
                    "ffmpeg-master-latest-win64-lgpl-shared/bin/avformat-61.dll",
                ])
            };
            var locator = new FfmpegLocator([], toolsDir, downloader.DownloadAsync);
            var resolved = await locator.LocateAsync();
            Assert.True(File.Exists(Path.Combine(toolsDir, "avcodec-61.dll")));
            Assert.True(File.Exists(Path.Combine(toolsDir, "avformat-61.dll")));
            Assert.Equal(1, downloader.Calls);
        }
        finally
        {
            Directory.Delete(toolsDir, recursive: true);
        }
    }

    [Fact]
    public async Task Locate_EmptyPayload_Throws()
    {
        var downloader = new RecordingDownloader { Payload = Array.Empty<byte>() };
        var locator = new FfmpegLocator([], TempDir(), downloader.DownloadAsync);
        await Assert.ThrowsAsync<IOException>(() => locator.LocateAsync());
        Assert.Equal(1, downloader.Calls);
    }

    [Fact]
    public async Task Locate_ZipWithoutFfmpegEntry_Throws()
    {
        var downloader = new RecordingDownloader { Payload = MakeZip(exePath: "readme.txt") };
        var locator = new FfmpegLocator([], TempDir(), downloader.DownloadAsync);
        await Assert.ThrowsAsync<InvalidDataException>(() => locator.LocateAsync());
    }

    [Fact]
    public async Task Locate_DownloaderFailure_Propagates()
    {
        var downloader = new RecordingDownloader { Error = new HttpRequestException("offline") };
        var locator = new FfmpegLocator([], TempDir(), downloader.DownloadAsync);
        await Assert.ThrowsAsync<HttpRequestException>(() => locator.LocateAsync());
    }
}
