using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using Jellyfin.Plugin.JRay.Models;
using Jellyfin.Plugin.JRay.Services;
using Xunit;
namespace Jellyfin.Plugin.JRay.Tests;
///
/// JR-004 (windows are scene-membership claims, served verbatim), JR-005 (query
/// semantics and inclusive bounds) and JR-006 (numerous windows).
///
/// These pin the semantics SR-002 sets. The failure they exist to prevent is a
/// well-meaning "tidy-up" — merging adjacent windows, trimming a zero-length
/// one, or collapsing overlaps — each of which silently answers a different
/// question from the one the truth file asked.
///
/// TRACES: UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023 | JR-004, JR-005, JR-006
///
public class PresenceLookupTests
{
private static TruthActor Actor(params double[][] windows)
{
var actor = new TruthActor { Name = "Steve Buscemi", TmdbId = "884" };
foreach (var w in windows)
{
actor.Scenes.Add(new TruthScene { Start = w[0], End = w[1] });
}
return actor;
}
// UT-016
[Theory]
[InlineData(12.0)] // exactly the start
[InlineData(30.0)] // inside
[InlineData(45.0)] // exactly the end
public void IsPresentAt_WithinInclusiveBounds_IsPresent(double t)
{
// Both ends inclusive: a window is [start, end], not [start, end).
Assert.True(PresenceLookup.IsPresentAt(Actor([12.0, 45.0]), t));
}
// UT-017
[Theory]
[InlineData(11.999)]
[InlineData(45.001)]
public void IsPresentAt_OutsideBounds_IsAbsent(double t)
{
Assert.False(PresenceLookup.IsPresentAt(Actor([12.0, 45.0]), t));
}
// UT-018
[Fact]
public void IsPresentAt_ZeroLengthWindow_IsPresentAtThatInstant()
{
// A single sighting is a legitimate window. Discarding it as degenerate
// would drop the actor from a scene they are demonstrably in.
Assert.True(PresenceLookup.IsPresentAt(Actor([30.0, 30.0]), 30.0));
}
// UT-019
[Fact]
public void IsPresentAt_OverlappingWindows_IsPresentInsideTheEnclosingOne()
{
// [0,100] encloses [50,60]. A lookup that assumed non-overlapping,
// sorted windows and stopped at the first start > t would miss t = 80.
Assert.True(PresenceLookup.IsPresentAt(Actor([0.0, 100.0], [50.0, 60.0]), 80.0));
}
// UT-020
[Fact]
public void IsPresentAt_UnsortedWindows_StillFindsPresence()
{
// Sortedness is a producer guarantee, not something correctness may
// depend on. A file that violates it must still be read correctly.
var actor = Actor([100.0, 110.0], [10.0, 20.0]);
Assert.True(PresenceLookup.IsPresentAt(actor, 15.0));
Assert.False(PresenceLookup.WindowsAreSorted(actor));
}
// UT-021
[Fact]
public void ActorsPresentAt_AdjacentWindowsAreNeverMerged()
{
// [0,10] and [10,20] look mergeable. They must not be merged: two
// windows mean a genuine departure and return, and the plugin does not
// reinterpret that claim. The actor is reported once, from two windows.
var truth = new TruthFile();
truth.Actors.Add(Actor([0.0, 10.0], [10.0, 20.0]));
Assert.Single(PresenceLookup.ActorsPresentAt(truth, 10.0));
Assert.Equal(2, truth.Actors[0].Scenes.Count);
}
// UT-022
[Fact]
public void TruthFile_RoundTrips_WithWindowsByteIdentical()
{
// JR-004: served exactly as given. A round trip through the serializer
// is where a silent normalisation would show up.
const string Json = """
{"schema_version":2,"movie":"/m.mkv",
"extraction":{"sample_fps":1,"extinction_sec":12},
"cut":{"runtime_sec":6420.5},
"actors":[{"name":"A","imdb_id":"","tmdb_id":"884","jellyfin_id":"",
"scenes":[{"start":0.0,"end":10.0},
{"start":10.0,"end":20.0},
{"start":30.0,"end":30.0}]}]}
""";
var parsed = JsonSerializer.Deserialize(Json, new JsonSerializerOptions(JsonSerializerDefaults.Web))!;
var windows = parsed.Actors[0].Scenes;
Assert.Equal(3, windows.Count);
Assert.Equal((0.0, 10.0), (windows[0].Start, windows[0].End));
Assert.Equal((10.0, 20.0), (windows[1].Start, windows[1].End));
Assert.Equal((30.0, 30.0), (windows[2].Start, windows[2].End));
}
// UT-023
[Fact]
public void ActorsPresentAt_WithManyWindows_StaysCheapAndBoundsTheResponse()
{
// SR-002: windows may be numerous; consumers must not assume a handful
// of long ones. Track-extent presence with a short re-acquisition
// timeout produces many short windows per actor.
var truth = new TruthFile();
for (var a = 0; a < 50; a++)
{
var actor = Actor();
for (var w = 0; w < 1000; w++)
{
actor.Scenes.Add(new TruthScene { Start = w * 10.0, End = (w * 10.0) + 4.0 });
}
truth.Actors.Add(actor);
}
var sw = Stopwatch.StartNew();
var present = PresenceLookup.ActorsPresentAt(truth, 5002.0).ToList();
sw.Stop();
// The response is bounded by actor count, never by window count — which
// is what keeps `jray?t=` small however finely presence is sliced.
Assert.Equal(50, present.Count);
// 50 000 windows scanned. Generous bound: this asserts the read path is
// not accidentally quadratic, not a precise budget on a shared runner.
Assert.True(sw.ElapsedMilliseconds < 250, $"lookup took {sw.ElapsedMilliseconds} ms");
}
}