using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;

/// <summary>
/// Tests for the process-wide <see cref="SiteScriptCompileCache"/>. The cache is
/// static/process-wide state, so this class shares the "SiteScriptCompileCache" xunit
/// collection with <c>ScriptCompilationServiceTests</c> (no cross-class parallelism) and
/// calls <see cref="SiteScriptCompileCache.Clear"/> at the top of each test.
/// </summary>
[Collection("SiteScriptCompileCache")]
public class SiteScriptCompileCacheTests
{
    private static ScriptCompilationResult Ok() =>
        ScriptCompilationResult.Succeeded(CSharpScript.Create<object?>("1", ScriptOptions.Default, typeof(ScriptGlobals)));

    [Fact]
    public void GetOrAdd_SameCodeAndGlobals_ComputesOnce()
    {
        SiteScriptCompileCache.Clear();
        var factoryCalls = 0;
        ScriptCompilationResult Factory() { factoryCalls++; return Ok(); }

        var r1 = SiteScriptCompileCache.GetOrAdd("return 1;", typeof(ScriptGlobals), Factory);
        var r2 = SiteScriptCompileCache.GetOrAdd("return 1;", typeof(ScriptGlobals), Factory);

        Assert.Equal(1, factoryCalls);
        Assert.Same(r1, r2);
        Assert.Equal(1, SiteScriptCompileCache.Hits);
    }

    [Fact]
    public void GetOrAdd_SameCode_DifferentGlobals_AreSeparateEntries()
    {
        SiteScriptCompileCache.Clear();
        SiteScriptCompileCache.GetOrAdd("1 > 0", typeof(ScriptGlobals), Ok);
        SiteScriptCompileCache.GetOrAdd("1 > 0", typeof(TriggerExpressionGlobals), Ok);

        Assert.Equal(2, SiteScriptCompileCache.Count);
        Assert.Equal(0, SiteScriptCompileCache.Hits); // no cross-globals hit
    }

    [Fact]
    public void Overflow_StaysWithinTheBound()
    {
        SiteScriptCompileCache.Clear();
        for (var i = 0; i <= SiteScriptCompileCache.MaxEntries; i++)
            SiteScriptCompileCache.GetOrAdd($"return {i};", typeof(ScriptGlobals), Ok);

        Assert.True(SiteScriptCompileCache.Count <= SiteScriptCompileCache.MaxEntries);
    }

    // ── WP3.1: approximate LRU replaced the wholesale-Clear overflow cliff ──────────

    /// <summary>
    /// WP3.1 (test group 8): overflow must evict only the OLDEST batch, keeping hot entries.
    /// The old behaviour cleared all 1024 entries, so the very next deploy or Instance-Actor
    /// start paid a full recompile storm on actor threads.
    /// </summary>
    [Fact]
    public void Overflow_EvictsOldestBatch_AndKeepsRecentlyTouchedEntries()
    {
        // Note: the cache is process-wide static and other test classes compile into it
        // concurrently, so the assertions below are phrased as properties of the eviction
        // policy (what survives, what does not, nothing is wiped) rather than as exact
        // counts, which no test can own here.
        SiteScriptCompileCache.Clear();

        // Fill to the bound. Entries 1..N are inserted oldest-first.
        for (var i = 0; i < SiteScriptCompileCache.MaxEntries; i++)
            SiteScriptCompileCache.GetOrAdd($"return {i};", typeof(ScriptGlobals), Ok);

        // Touch entry 0 so it carries the NEWEST access stamp despite being inserted first.
        var hot = SiteScriptCompileCache.GetOrAdd("return 0;", typeof(ScriptGlobals), Ok);
        var hitsAfterTouch = SiteScriptCompileCache.Hits;

        // Push well past the bound so at least one eviction sweep definitely runs.
        for (var i = 0; i < 200; i++)
            SiteScriptCompileCache.GetOrAdd($"return overflow{i};", typeof(ScriptGlobals), Ok);

        // Nothing was cleared wholesale: the cache is still most of the way full and the hit
        // counter survived (Clear() would have reset it to 0).
        Assert.True(SiteScriptCompileCache.Count > SiteScriptCompileCache.MaxEntries / 2,
            $"cache collapsed to {SiteScriptCompileCache.Count} entries — this looks like a wholesale clear");
        Assert.True(SiteScriptCompileCache.Hits >= hitsAfterTouch);

        // The touched entry survived the sweep — a hit, not a recompile. This is the LRU
        // property: recency, not insertion order, decides what stays.
        var hitsBefore = SiteScriptCompileCache.Hits;
        var again = SiteScriptCompileCache.GetOrAdd("return 0;", typeof(ScriptGlobals),
            () => throw new InvalidOperationException("hot entry was evicted — LRU is not keeping recently-used entries"));
        Assert.Same(hot, again);
        Assert.Equal(hitsBefore + 1, SiteScriptCompileCache.Hits);

        // …and untouched old entries genuinely were evicted, so the sweep really ran.
        var recomputed = 0;
        for (var i = 1; i <= 16; i++)
        {
            var code = $"return {i};";
            SiteScriptCompileCache.GetOrAdd(code, typeof(ScriptGlobals),
                () => { recomputed++; return Ok(); });
        }
        Assert.True(recomputed > 0, "no old entry was evicted — the overflow sweep did not run");
    }

    /// <summary>
    /// WP3.1: concurrent inserts crossing the bound together must not blow past it — the
    /// eviction sweep is double-checked under its own lock precisely so a stampede performs
    /// one sweep rather than one per racing thread.
    /// </summary>
    [Fact]
    public async Task ConcurrentGetOrAddStorm_KeepsTheBound()
    {
        SiteScriptCompileCache.Clear();

        const int perTask = 400;
        var tasks = Enumerable.Range(0, 8).Select(t => Task.Run(() =>
        {
            for (var i = 0; i < perTask; i++)
                SiteScriptCompileCache.GetOrAdd($"return {t}_{i};", typeof(ScriptGlobals), Ok);
        }));

        await Task.WhenAll(tasks);

        // A small transient overshoot is by design: threads that lose the double-checked
        // eviction race still insert their own entry afterwards, so the ceiling is "bounded",
        // not "never exceeded by one". The property under test is that 3200 distinct inserts
        // do not accumulate — before WP3.1 this was a wholesale Clear(), and a per-entry
        // eviction bug would show up here as unbounded growth, not as a handful of extra rows.
        // (Other test classes compile into this process-wide cache concurrently, which is the
        // other reason the bound is asserted with slack rather than exactly.)
        Assert.True(SiteScriptCompileCache.Count <= SiteScriptCompileCache.MaxEntries + 512,
            $"cache grew past its bound: {SiteScriptCompileCache.Count} vs max {SiteScriptCompileCache.MaxEntries} " +
            $"after {8 * perTask} distinct inserts");
    }
}
