using Mbproxy.Proxy.Cache;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Proxy.Cache;

/// <summary>
/// Cover invariants of the <see cref="CacheEntry"/> record: TTL boundary expiry, byte
/// independence (caller-supplied <c>PduBytes</c> are not mutated by the cache itself —
/// the cache copies on store), and monotonic <see cref="CacheEntry.LastUsedTick"/>
/// semantics when the cache stamps it.
/// </summary>
[Trait("Category", "Unit")]
public sealed class CacheEntryTests
{
    [Fact]
    public void Expired_When_NowEqualsOrExceedsExpiresAtUtc()
    {
        var now = DateTimeOffset.UtcNow;
        var entry = new CacheEntry(
            PduBytes:     [0x03, 0x02, 0x04, 0xD2],
            CachedAtUtc:  now,
            ExpiresAtUtc: now.AddMilliseconds(50),
            Length:       4,
            LastUsedTick: 1);

        // The entry exposes ExpiresAtUtc for the cache to compare against UtcNow. Sanity:
        // an entry whose ExpiresAtUtc is in the past is expired.
        var past = entry with { ExpiresAtUtc = now.AddMilliseconds(-1) };
        (past.ExpiresAtUtc <= now).ShouldBeTrue("an entry whose expiry is in the past must be expired");

        var future = entry with { ExpiresAtUtc = now.AddMilliseconds(100) };
        (future.ExpiresAtUtc > now).ShouldBeTrue("an entry whose expiry is in the future must be live");
    }

    [Fact]
    public void Record_With_Expression_DoesNotMutate_OriginalArrayContents()
    {
        var bytes = new byte[] { 0x03, 0x02, 0x04, 0xD2 };
        var entry = new CacheEntry(
            PduBytes:     bytes,
            CachedAtUtc:  DateTimeOffset.UtcNow,
            ExpiresAtUtc: DateTimeOffset.UtcNow.AddSeconds(1),
            Length:       4,
            LastUsedTick: 1);

        // Sanity: the entry holds a reference to the supplied array. The cache's hot path
        // never mutates PduBytes; this test pins that contract by mutating the original
        // array and confirming the entry sees the change (i.e. it doesn't copy on store,
        // but the cache wraps Set with a snapshot — verified separately in the multiplexer
        // path).
        bytes[0] = 0xFF;
        entry.PduBytes[0].ShouldBe((byte)0xFF, "CacheEntry holds the supplied reference; defensive copies live in the multiplexer");
    }

    [Fact]
    public void LastUsedTick_Stamped_By_ResponseCache_OnSet_AndOnHit()
    {
        // The CacheEntry itself doesn't compute LastUsedTick — the cache assigns the next
        // tick on every Set/Get. Verified here in conjunction with the cache: two inserts
        // produce strictly-increasing ticks; a hit refreshes the tick.
        using var cache = new ResponseCache(maxEntriesPerPlc: 10, evictionIntervalMs: 5000);
        var k1 = new CacheKey(1, 0x03, 100, 1);
        var k2 = new CacheKey(1, 0x03, 200, 1);

        cache.Set(k1, MakeEntry(ttlMs: 1000));
        cache.Set(k2, MakeEntry(ttlMs: 1000));

        cache.TryGet(k1, out var e1).ShouldBeTrue();
        cache.TryGet(k2, out var e2).ShouldBeTrue();

        // Whichever was touched last has the larger LastUsedTick (k2 was the most recent
        // touch via TryGet).
        e2.LastUsedTick.ShouldBeGreaterThan(e1.LastUsedTick, "the more-recently-touched entry must carry the larger tick");
    }

    private static CacheEntry MakeEntry(int ttlMs)
    {
        var now = DateTimeOffset.UtcNow;
        return new CacheEntry(
            PduBytes:     [0x03, 0x02, 0x04, 0xD2],
            CachedAtUtc:  now,
            ExpiresAtUtc: now.AddMilliseconds(ttlMs),
            Length:       4,
            LastUsedTick: 0);
    }
}
