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

namespace Mbproxy.Tests.Proxy.Cache;

/// <summary>
/// Six range-overlap unit tests required by the Phase-11 doc. Half-open interval math:
/// write [w, w+writeQty) overlaps entry [s, s+qty) iff w &lt; s+qty AND s &lt; w+writeQty.
/// </summary>
[Trait("Category", "Unit")]
public sealed class CacheInvalidatorTests
{
    private static CacheKey K(byte unit, ushort start, ushort qty, byte fc = 0x03)
        => new(unit, fc, start, qty);

    [Fact]
    public void FullOverlap_WriteCoversEntryRange_Invalidates()
    {
        // Entry [100..110), write [95..115) — write covers entry fully.
        var entry = K(unit: 1, start: 100, qty: 10);
        var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 95, writeQty: 20).ToList();

        hits.ShouldContain(entry, "a write that fully contains the entry's range must invalidate it");
    }

    [Fact]
    public void PartialOverlap_WriteStartsBeforeEntry_Invalidates()
    {
        // Entry [100..110), write [95..105) — overlaps low side.
        var entry = K(unit: 1, start: 100, qty: 10);
        var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 95, writeQty: 10).ToList();

        hits.ShouldContain(entry, "low-side partial overlap must invalidate");
    }

    [Fact]
    public void PartialOverlap_WriteEndsAfterEntry_Invalidates()
    {
        // Entry [100..110), write [105..115) — overlaps high side.
        var entry = K(unit: 1, start: 100, qty: 10);
        var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 105, writeQty: 10).ToList();

        hits.ShouldContain(entry, "high-side partial overlap must invalidate");
    }

    [Fact]
    public void Adjacent_NotOverlapping_DoesNotInvalidate()
    {
        // Half-open intervals: write [10..15) is adjacent to but NOT overlapping entry
        // [15..20) — register 15 is in the entry but NOT in the write. Should not match.
        var entry = K(unit: 1, start: 15, qty: 5);
        var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 10, writeQty: 5).ToList();

        hits.ShouldBeEmpty("adjacent-but-not-overlapping ranges must not invalidate (half-open semantics)");
    }

    [Fact]
    public void NoOverlap_DoesNotInvalidate()
    {
        // Entry [100..110), write [200..210) — fully disjoint.
        var entry = K(unit: 1, start: 100, qty: 10);
        var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 200, writeQty: 10).ToList();

        hits.ShouldBeEmpty("disjoint ranges must not invalidate");
    }

    [Fact]
    public void DifferentUnitId_DoesNotInvalidate()
    {
        // Same address range, different unit ID — must not match.
        var entry = K(unit: 1, start: 100, qty: 10);
        var hits = CacheInvalidator.FindOverlapping([entry], unitId: 2, writeStart: 95, writeQty: 20).ToList();

        hits.ShouldBeEmpty("writes on a different unit ID must not invalidate this entry");
    }

    // ── Auxiliary correctness checks ─────────────────────────────────────────────

    [Fact]
    public void FcOtherThan03Or04_NeverInvalidated()
    {
        // Defensive: only FC03/FC04 entries are ever stored, but if a non-read key
        // somehow appeared the invalidator must skip it.
        var nonRead = new CacheKey(UnitId: 1, Fc: 0x06, StartAddress: 100, Qty: 10);
        var hits = CacheInvalidator.FindOverlapping([nonRead], unitId: 1, writeStart: 95, writeQty: 20).ToList();

        hits.ShouldBeEmpty("only FC03/FC04 entries should ever be invalidated");
    }

    [Fact]
    public void ZeroWriteQty_NeverInvalidates()
    {
        var entry = K(unit: 1, start: 100, qty: 10);
        var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 100, writeQty: 0).ToList();

        hits.ShouldBeEmpty("a degenerate write covering zero registers must not invalidate anything");
    }
}
