using AcDream.App.Audio;
namespace AcDream.App.Tests.Audio;
///
/// Pure LRU/byte-budget decision logic backing
/// 's bounded AL buffer cache (verified-
/// unbounded audit, 2026-07-24). The engine's actual AL calls
/// (alGenBuffers, alDeleteBuffers, source binding) aren't
/// unit-testable headless — OpenAlResourceLifetimeTests' fake
/// IOpenAlResourceApi deliberately exposes a null AL, which
/// short-circuits every native buffer call before it runs. This suite
/// covers the pure eviction/budget decision that was extracted specifically
/// so it stays testable without a live OpenAL device.
///
public sealed class AlBufferBudgetTrackerTests
{
[Fact]
public void RecordCreated_TracksResidentBytesAndCount()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
tracker.RecordCreated(waveId: 1, bufferId: 101, bytes: 200);
tracker.RecordCreated(waveId: 2, bufferId: 102, bytes: 300);
Assert.Equal(500, tracker.ResidentBytes);
Assert.Equal(2, tracker.Count);
Assert.True(tracker.TryGetBufferId(1, out uint buf1));
Assert.Equal(101u, buf1);
Assert.True(tracker.TryGetBufferId(2, out uint buf2));
Assert.Equal(102u, buf2);
}
[Fact]
public void TryGetBufferId_UnknownWaveId_ReturnsFalse()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
Assert.False(tracker.TryGetBufferId(42, out uint bufferId));
Assert.Equal(0u, bufferId);
}
[Fact]
public void TryEvictOldestUnprotected_PicksLeastRecentlyUsedEntry()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
tracker.RecordCreated(1, 101, 100); // oldest
tracker.RecordCreated(2, 102, 100);
tracker.RecordCreated(3, 103, 100); // newest
bool evicted = tracker.TryEvictOldestUnprotected(
static _ => false, out uint waveId, out uint bufferId);
Assert.True(evicted);
Assert.Equal(1u, waveId);
Assert.Equal(101u, bufferId);
Assert.Equal(200, tracker.ResidentBytes);
Assert.Equal(2, tracker.Count);
Assert.False(tracker.TryGetBufferId(1, out _));
}
[Fact]
public void Touch_MovesEntryToMostRecentlyUsed()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
tracker.RecordCreated(1, 101, 100);
tracker.RecordCreated(2, 102, 100);
tracker.RecordCreated(3, 103, 100);
// Without a touch, wave 1 (buffer 101) is oldest and would be evicted.
tracker.Touch(1);
bool evicted = tracker.TryEvictOldestUnprotected(
static _ => false, out uint waveId, out _);
Assert.True(evicted);
Assert.Equal(2u, waveId); // 2 is now the least-recently-used, not 1
}
[Fact]
public void Touch_UnknownWaveId_IsNoOp()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
tracker.RecordCreated(1, 101, 100);
tracker.Touch(999); // never recorded — must not throw or create an entry
Assert.Equal(1, tracker.Count);
}
[Fact]
public void TryEvictOldestUnprotected_SkipsProtectedEntriesAndPicksNextOldest()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
tracker.RecordCreated(1, 101, 100); // oldest, but protected (attached to a live source)
tracker.RecordCreated(2, 102, 100); // next-oldest, unprotected — should be picked
tracker.RecordCreated(3, 103, 100); // newest
bool evicted = tracker.TryEvictOldestUnprotected(
bufferId => bufferId == 101, out uint waveId, out uint bufferIdOut);
Assert.True(evicted);
Assert.Equal(2u, waveId);
Assert.Equal(102u, bufferIdOut);
Assert.True(tracker.TryGetBufferId(1, out _)); // still resident — protected
}
[Fact]
public void TryEvictOldestUnprotected_EverythingProtected_ReturnsFalseAndChangesNothing()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
tracker.RecordCreated(1, 101, 100);
tracker.RecordCreated(2, 102, 100);
bool evicted = tracker.TryEvictOldestUnprotected(
static _ => true, out uint waveId, out uint bufferId);
Assert.False(evicted);
Assert.Equal(0u, waveId);
Assert.Equal(0u, bufferId);
Assert.Equal(200, tracker.ResidentBytes);
Assert.Equal(2, tracker.Count);
}
[Fact]
public void TryEvictOldestUnprotected_EmptyTracker_ReturnsFalse()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
bool evicted = tracker.TryEvictOldestUnprotected(
static _ => false, out _, out _);
Assert.False(evicted);
}
[Fact]
public void RecordCreated_ZeroOrNegativeBytes_IsChargedAtLeastOneByte()
{
var tracker = new AlBufferBudgetTracker(maxBytes: 1000);
tracker.RecordCreated(1, 101, bytes: 0);
Assert.Equal(1, tracker.ResidentBytes);
}
[Fact]
public void Constructor_RejectsNonPositiveBudget()
{
Assert.Throws(() => new AlBufferBudgetTracker(0));
Assert.Throws(() => new AlBufferBudgetTracker(-1));
}
}