using System;
using System.IO;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Services;
using ytLive.ViewModels;
namespace ytLive.Tests;
///
/// Record-OR-live (creator ruling 2026-09-01): the REC and ON-AIR pills are
/// radio-exclusive — arming one clears the other, so a session can never dual-encode
/// to both the YouTube ingest and a local file at once (a concurrent disk write
/// streamed from the same sustained Kbps cripples the stream on mid-range chassis).
/// The dual-block encoder machinery in FfmpegArgs stays generic; the constraint
/// lives at the pills.
///
[Collection("RealApp")]
public sealed class PillRadioTests
{
private readonly RealAppHost _app;
public PillRadioTests(RealAppHost app) => _app = app;
[Fact]
public void Arming_One_Output_Pill_Clears_The_Other()
{
_app.Run(Run);
}
private void Run()
{
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-pillradio-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
try
{
using (var schema = new LayoutStore(tempDb)) { }
SqliteConnection.ClearAllPools();
var vm = new MainViewModel();
vm.IsConnected = true; // the ON-AIR setter requires a sign-in
// Arm REC first, then ON-AIR → REC must clear.
vm.RecordPillOn = true;
vm.OnAirPillOn = true;
Assert.False(vm.RecordPillOn, "arming ON-AIR must clear the REC pill");
Assert.True(vm.OnAirPillOn);
// Arm REC while ON-AIR is lit → ON-AIR must clear.
vm.RecordPillOn = true;
Assert.False(vm.OnAirPillOn, "arming REC must clear the ON-AIR pill");
Assert.True(vm.RecordPillOn);
// A single armed pill still makes the Start button actionable.
Assert.True(vm.CanStartSession);
}
finally
{
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
}
}
/// Good Dog (one integration test, creator ruling 2026-09-20): the ON-AIR
/// pill is armable signed-out — intent may light while the account is offline (a
/// greyed pill was the dead end the guard shipped to forbid). A front-branch on
/// the ON-AIR setter that dropped the pill headlessly is what the pill is FOR.
/// Since the top bar redesign, the Go face names the armed WORLD ("Go Live")
/// rather than the account race — a signed-out Stream press routes into the
/// sign-in inside StartSession, and the dedicated Sign In button appears on the
/// right exactly when the stream sequence needs the account step.
[Fact]
public void SignedOut_OnAir_Pill_Stays_Lit_And_Stream_World_Is_GoLive()
{
_app.Run(RunSignedOutOnAir);
}
private void RunSignedOutOnAir()
{
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-pillradio-signin-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
try
{
using (var schema = new LayoutStore(tempDb)) { }
SqliteConnection.ClearAllPools();
var vm = new MainViewModel();
// The dev machine may hold a real saved OAuth session (loads synchronously
// during the ctor), so the signed-out face is forced deterministically.
vm.IsConnected = false;
// No sign-in: the ON-AIR pill MUST still be able to light — intent before
// reality, armable signed-out (creator ruling 2026-09-20).
vm.OnAirPillOn = true;
Assert.True(vm.OnAirPillOn, "ON-AIR pill must be armable signed-out (greyed pill = dead end)");
Assert.True(vm.CanStartSession);
// While armed signed-out the Go face names the Stream world ("Go Live");
// the account step surfaces as the Sign In button on the right instead,
// and a signed-out start routes into the sign-in (never a dead-end go-live).
Assert.Equal("Go Live", vm.PrimaryStartButtonLabel);
Assert.True(vm.ShowSignInButton);
Assert.False(vm.ShowRecordAction);
Assert.True(vm.ShowStreamActions);
// A session connects: the pill KEEPS its light; the stream world's Sign In
// retires and the avatar confirms the account.
vm.IsConnected = true;
Assert.True(vm.OnAirPillOn, "the armed signed-out pill must survive the sign-in");
Assert.Equal("Go Live", vm.PrimaryStartButtonLabel);
Assert.False(vm.ShowSignInButton);
Assert.True(vm.ShowAvatarButton);
}
finally
{
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
}
}
}