using AmiReel.Models;
using AmiReel.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AmiReel.Tests.Services;

[TestClass]
public class RenderPipelineTests
{
    private readonly List<string> _tempDirectories = [];

    [TestCleanup]
    public void Cleanup()
    {
        foreach (string directory in _tempDirectories)
        {
            try { Directory.Delete(directory, true); } catch { }
        }
    }

    private string CreateTempDirectory()
    {
        string directory = Path.Combine(Path.GetTempPath(), "AmiReelTests", Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(directory);
        _tempDirectories.Add(directory);
        return directory;
    }

    private static RenderSettings ValidSettings(IReadOnlyList<string> inputFiles, string outputDirectory, string endCardPath = "") => new()
    {
        InputFiles = inputFiles,
        EndCardPath = endCardPath,
        OutputDirectory = outputDirectory,
        OutputName = "output",
        FfmpegPath = "",
        FfprobePath = "",
        Encoder = EncoderMode.Auto,
        TrimStart = 0,
        FadeSeconds = 1,
        EndCardHoldSeconds = 1,
        ThumbnailInterval = 10,
    };

    [TestMethod]
    public void Validate_WithNoInputFiles_ThrowsArgumentException()
    {
        // Arrange
        RenderSettings settings = ValidSettings([], @"C:\out");

        // Act & Assert
        Assert.ThrowsExactly<ArgumentException>(() => RenderPipeline.Validate(settings));
    }

    [TestMethod]
    public void Validate_WithMissingInputFile_ThrowsFileNotFoundException()
    {
        // Arrange
        string directory = CreateTempDirectory();
        RenderSettings settings = ValidSettings([Path.Combine(directory, "missing.avi")], directory);

        // Act & Assert
        Assert.ThrowsExactly<FileNotFoundException>(() => RenderPipeline.Validate(settings));
    }

    [TestMethod]
    public void Validate_WithMissingEndCard_ThrowsFileNotFoundException()
    {
        // Arrange
        string directory = CreateTempDirectory();
        string input = Path.Combine(directory, "clip.avi");
        File.WriteAllText(input, "data");
        RenderSettings settings = ValidSettings([input], directory, endCardPath: Path.Combine(directory, "missing.png"));

        // Act & Assert
        Assert.ThrowsExactly<FileNotFoundException>(() => RenderPipeline.Validate(settings));
    }

    [TestMethod]
    public void Validate_WithBlankOutputDirectory_ThrowsArgumentException()
    {
        // Arrange
        string directory = CreateTempDirectory();
        string input = Path.Combine(directory, "clip.avi");
        File.WriteAllText(input, "data");
        RenderSettings settings = ValidSettings([input], "");

        // Act & Assert
        Assert.ThrowsExactly<ArgumentException>(() => RenderPipeline.Validate(settings));
    }

    [TestMethod]
    public void Validate_WithInvalidOutputName_ThrowsArgumentException()
    {
        // Arrange
        string directory = CreateTempDirectory();
        string input = Path.Combine(directory, "clip.avi");
        File.WriteAllText(input, "data");
        RenderSettings settings = ValidSettings([input], directory) with { OutputName = "bad" + Path.GetInvalidFileNameChars()[0] };

        // Act & Assert
        Assert.ThrowsExactly<ArgumentException>(() => RenderPipeline.Validate(settings));
    }

    [TestMethod]
    public void Validate_WithThumbnailIntervalLessThanOne_ThrowsArgumentException()
    {
        // Arrange
        string directory = CreateTempDirectory();
        string input = Path.Combine(directory, "clip.avi");
        File.WriteAllText(input, "data");
        RenderSettings settings = ValidSettings([input], directory) with { ThumbnailInterval = 0 };

        // Act & Assert
        Assert.ThrowsExactly<ArgumentException>(() => RenderPipeline.Validate(settings));
    }

    [TestMethod]
    public void Validate_WithAllRequirementsMet_DoesNotThrow()
    {
        // Arrange
        string directory = CreateTempDirectory();
        string input = Path.Combine(directory, "clip.avi");
        File.WriteAllText(input, "data");
        RenderSettings settings = ValidSettings([input], directory);

        // Act & Assert (no exception)
        RenderPipeline.Validate(settings);
    }

    [TestMethod]
    public void BuildProgressMessage_WithOnlyTime_ReturnsTimeRangeOnly()
    {
        // Act
        string message = RenderPipeline.BuildProgressMessage(TimeSpan.FromSeconds(30), 60, null, null, null, 50);

        // Assert
        Assert.AreEqual("00:00:30 / 00:01:00", message);
    }

    [TestMethod]
    public void BuildProgressMessage_WithAllOptionalFields_IncludesFpsSpeedAndFrame()
    {
        // Arrange: numbers render using the current culture (this is user-facing text),
        // so build the expected fragments the same way rather than hard-coding "." / ",".
        const double fps = 49.8;
        const double speed = 0.99;
        const int frame = 1500;
        const int totalFrames = 3000; // ceil(60s * 50fps)

        // Act
        string message = RenderPipeline.BuildProgressMessage(TimeSpan.FromSeconds(30), 60, fps, speed, frame, 50);

        // Assert
        StringAssert.Contains(message, "00:00:30 / 00:01:00");
        StringAssert.Contains(message, $"{fps:0.#} fps");
        StringAssert.Contains(message, $"{speed:0.##}x");
        StringAssert.Contains(message, $"frame {frame:N0} / {totalFrames:N0}");
    }

    [TestMethod]
    public void MoveSourceVideos_WithNewDestination_MovesFileIntoOriginalsSubfolder()
    {
        // Arrange
        string outputDirectory = CreateTempDirectory();
        string sourceDirectory = CreateTempDirectory();
        string source = Path.Combine(sourceDirectory, "clip.avi");
        File.WriteAllText(source, "data");
        List<string> logs = [];

        // Act
        IReadOnlyList<string> result = RenderPipeline.MoveSourceVideos([source], outputDirectory, logs.Add);

        // Assert
        string expected = Path.Combine(outputDirectory, "originals", "clip.avi");
        Assert.AreEqual(expected, result.Single());
        Assert.IsTrue(File.Exists(expected));
        Assert.IsFalse(File.Exists(source));
    }

    [TestMethod]
    public void MoveSourceVideos_WithDuplicateFilename_AppendsNumericSuffix()
    {
        // Arrange
        string outputDirectory = CreateTempDirectory();
        string sourceDirectory = CreateTempDirectory();
        string sourceA = Path.Combine(sourceDirectory, "a", "clip.avi");
        string sourceB = Path.Combine(sourceDirectory, "b", "clip.avi");
        Directory.CreateDirectory(Path.GetDirectoryName(sourceA)!);
        Directory.CreateDirectory(Path.GetDirectoryName(sourceB)!);
        File.WriteAllText(sourceA, "data-a");
        File.WriteAllText(sourceB, "data-b");

        // Act
        IReadOnlyList<string> result = RenderPipeline.MoveSourceVideos([sourceA, sourceB], outputDirectory, _ => { });

        // Assert
        Assert.AreEqual(2, result.Distinct(StringComparer.OrdinalIgnoreCase).Count());
        Assert.IsTrue(result.All(File.Exists));
    }

    [TestMethod]
    public void MoveSourceVideos_WithMissingSourceFile_LogsWarningAndKeepsOriginalPath()
    {
        // Arrange
        string outputDirectory = CreateTempDirectory();
        string missingSource = Path.Combine(CreateTempDirectory(), "gone.avi");
        List<string> logs = [];

        // Act
        IReadOnlyList<string> result = RenderPipeline.MoveSourceVideos([missingSource], outputDirectory, logs.Add);

        // Assert
        Assert.AreEqual(missingSource, result.Single());
        Assert.IsTrue(logs.Any(line => line.Contains("no longer exists", StringComparison.OrdinalIgnoreCase)));
    }
}
