using System.Diagnostics;
using ErsatzTV.Core.FFmpeg;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Core.Tests.FFmpeg;

[TestFixture]
public class ColdStartStartupSplitTests
{
    // milestones are Stopwatch.GetTimestamp() values; build them from a base + millisecond offsets
    private const long Base = 1_000_000_000;

    private static long At(double milliseconds) =>
        Base + (long)(milliseconds / 1000.0 * Stopwatch.Frequency);

    [Test]
    public void Should_Split_Three_Ways_When_All_Milestones_Present()
    {
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            At(0),
            At(150),
            At(1200),
            At(1600));

        split.Kind.ShouldBe(ColdStartStartupSplitKind.ThreeWay);
        split.Prep.TotalMilliseconds.ShouldBe(150, 1);
        split.FFmpegInit.TotalMilliseconds.ShouldBe(1050, 1);
        split.FirstGop.TotalMilliseconds.ShouldBe(400, 1);
    }

    [Test]
    public void Sub_Phases_Should_Sum_To_Run_Entry_Through_Playlist()
    {
        // deliberately NOT "should sum to startup": the buckets span the worker's Run entry, which
        // begins before the request thread's startup stopwatch, so prep overlaps the tail of setup
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            At(0),
            At(150),
            At(1200),
            At(1600));

        (split.Prep + split.FFmpegInit + split.FirstGop).TotalMilliseconds.ShouldBe(1600, 1);
    }

    [Test]
    public void Should_Be_Unavailable_When_The_Playlist_Predates_The_Process_Launch()
    {
        // a stale live.m3u8 survives when the handler's pre-session folder wipe fails (EmptyFolder
        // swallows the failure into a warning). Every bucket would be meaningless, so report nothing
        // rather than a plausible-looking sample with a prep that exceeds the whole measured phase
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            At(0),
            At(1600),
            0,
            At(150));

        split.ShouldBe(ColdStartStartupSplit.Unavailable);
    }

    [Test]
    public void Stale_Playlist_Guard_Should_Take_Precedence_Over_The_Progress_Branches()
    {
        // without the guard, this input would be classified TwoWayLateProgress; the guard must be
        // evaluated first. (It can never preempt a ThreeWay: that requires processLaunched <=
        // playlistExists, which is exactly the negation of the guard condition.)
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            At(0),
            At(1600),
            At(1700),
            At(150));

        split.ShouldBe(ColdStartStartupSplit.Unavailable);
    }

    [Test]
    public void Should_Fall_Back_To_Two_Way_Split_When_Progress_Predates_The_Process_Launch()
    {
        // a progress timestamp older than the launch cannot belong to this process
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            At(100),
            At(150),
            At(120),
            At(1600));

        split.Kind.ShouldBe(ColdStartStartupSplitKind.TwoWay);
        split.FFmpegInit.TotalMilliseconds.ShouldBe(1450, 1);
        split.FirstGop.ShouldBe(TimeSpan.Zero);
    }

    [Test]
    public void Should_Stay_Three_Way_When_Progress_Coincides_With_A_Boundary()
    {
        ColdStartStartupSplit atLaunch = ColdStartStartupSplit.FromTimestamps(At(0), At(150), At(150), At(1600));
        atLaunch.Kind.ShouldBe(ColdStartStartupSplitKind.ThreeWay);
        atLaunch.FFmpegInit.ShouldBe(TimeSpan.Zero);
        atLaunch.FirstGop.TotalMilliseconds.ShouldBe(1450, 1);

        ColdStartStartupSplit atPlaylist = ColdStartStartupSplit.FromTimestamps(At(0), At(150), At(1600), At(1600));
        atPlaylist.Kind.ShouldBe(ColdStartStartupSplitKind.ThreeWay);
        atPlaylist.FFmpegInit.TotalMilliseconds.ShouldBe(1450, 1);
        atPlaylist.FirstGop.ShouldBe(TimeSpan.Zero);
    }

    [Test]
    public void Should_Fall_Back_To_Two_Way_Split_When_FFmpeg_Never_Reported_Progress()
    {
        // no -progress output before the playlist appeared: ffmpegInit must absorb the remainder
        // rather than the split inventing a firstGop boundary that was never observed
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            At(0),
            At(150),
            0,
            At(1600));

        split.Kind.ShouldBe(ColdStartStartupSplitKind.TwoWay);
        split.Prep.TotalMilliseconds.ShouldBe(150, 1);
        split.FFmpegInit.TotalMilliseconds.ShouldBe(1450, 1);
        split.FirstGop.ShouldBe(TimeSpan.Zero);
    }

    [Test]
    public void Should_Report_Late_Progress_Distinctly_When_Progress_Arrived_After_The_Playlist()
    {
        // the playlist is observed on the request thread while progress is recorded on the worker
        // thread; a progress milestone outside the phase must not produce a negative bucket
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            At(0),
            At(150),
            At(1800),
            At(1600));

        split.Kind.ShouldBe(ColdStartStartupSplitKind.TwoWayLateProgress);
        split.FFmpegInit.TotalMilliseconds.ShouldBe(1450, 1);
        split.FirstGop.ShouldBe(TimeSpan.Zero);
    }

    [TestCase(0L, 150L, 1200L, 1600L, TestName = "Run never started")]
    [TestCase(100L, 0L, 0L, 1600L, TestName = "Process never launched")]
    [TestCase(100L, 150L, 1200L, 0L, TestName = "Playlist never appeared")]
    public void Should_Be_Unavailable_When_A_Required_Milestone_Is_Missing(
        long runStarted,
        long processLaunched,
        long firstProgress,
        long playlistExists)
    {
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            runStarted == 0 ? 0 : At(runStarted),
            processLaunched == 0 ? 0 : At(processLaunched),
            firstProgress == 0 ? 0 : At(firstProgress),
            playlistExists == 0 ? 0 : At(playlistExists));

        split.ShouldBe(ColdStartStartupSplit.Unavailable);
    }

    [Test]
    public void Should_Clamp_Rather_Than_Report_A_Negative_Prep()
    {
        // defensive: launch cannot precede Run entry, but telemetry must never show a negative
        ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
            At(500),
            At(150),
            At(1200),
            At(1600));

        split.Prep.ShouldBe(TimeSpan.Zero);
        split.Kind.ShouldBe(ColdStartStartupSplitKind.ThreeWay);
        split.FFmpegInit.TotalMilliseconds.ShouldBe(1050, 1);
        split.FirstGop.TotalMilliseconds.ShouldBe(400, 1);
    }
}
