using Akka.Actor;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

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

/// <summary>
/// M2.12 (#25): recursion-limit violations at <c>CallScript</c> and
/// <c>CallShared</c> must emit a <c>script</c> Error site event via
/// <see cref="ISiteEventLogger"/> in addition to the existing
/// <c>ILogger.LogError</c> + throw.
/// </summary>
public class RecursionLimitSiteEventTests
{
    private const string InstanceName = "test-instance";
    private const string SourceScript = "ScriptActor:OnTick";

    /// <summary>
    /// Build a <see cref="ScriptRuntimeContext"/> wired at the recursion
    /// limit (currentCallDepth == maxCallDepth) so the very next
    /// <c>CallScript</c> or <c>CallShared</c> call trips the guard.
    /// </summary>
    private static ScriptRuntimeContext CreateContextAtLimit(
        ISiteEventLogger siteEventLogger,
        int maxCallDepth = 3)
    {
        var compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        var sharedScriptLibrary = new SharedScriptLibrary(
            compilationService, NullLogger<SharedScriptLibrary>.Instance);

        return new ScriptRuntimeContext(
            ActorRefs.Nobody,
            sharedScriptLibrary,
            currentCallDepth: maxCallDepth,   // already AT the limit
            maxCallDepth: maxCallDepth,
            askTimeout: TimeSpan.FromSeconds(5),
            instanceName: InstanceName,
            logger: NullLogger.Instance,
            siteEventLogger: siteEventLogger);
    }

    // -------------------------------------------------------------------------
    // CallScript
    // -------------------------------------------------------------------------

    [Fact]
    public async Task CallScript_RecursionLimitExceeded_EmitsScriptErrorSiteEvent()
    {
        var loggerMock = new Mock<ISiteEventLogger>();
        loggerMock
            .Setup(l => l.LogEventAsync(
                It.IsAny<string>(), It.IsAny<string>(),
                It.IsAny<string?>(), It.IsAny<string>(),
                It.IsAny<string>(), It.IsAny<string?>()))
            .Returns(Task.CompletedTask);

        var context = CreateContextAtLimit(loggerMock.Object);

        // The call must throw — recursion limit hit
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => context.CallScript("AnyScript"));

        // Give the fire-and-forget task a moment to complete
        await Task.Yield();

        loggerMock.Verify(l => l.LogEventAsync(
            "script",
            "Error",
            InstanceName,
            It.IsAny<string>(),        // source (may be null when sourceScript not set)
            It.Is<string>(m => m.Contains("CallScript") && m.Contains("depth")),
            null),
            Times.Once);
    }

    [Fact]
    public async Task CallScript_RecursionLimitExceeded_NullLogger_DoesNotThrow()
    {
        // Null siteEventLogger — existing behaviour must be fully unchanged:
        // LogError + throw, no NullReferenceException from the emission path.
        var context = CreateContextAtLimit(siteEventLogger: null!);

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => context.CallScript("AnyScript"));

        Assert.Contains("depth", ex.Message);
    }

    // -------------------------------------------------------------------------
    // CallShared (via Scripts.CallShared)
    // -------------------------------------------------------------------------

    [Fact]
    public async Task CallShared_RecursionLimitExceeded_EmitsScriptErrorSiteEvent()
    {
        var loggerMock = new Mock<ISiteEventLogger>();
        loggerMock
            .Setup(l => l.LogEventAsync(
                It.IsAny<string>(), It.IsAny<string>(),
                It.IsAny<string?>(), It.IsAny<string>(),
                It.IsAny<string>(), It.IsAny<string?>()))
            .Returns(Task.CompletedTask);

        var context = CreateContextAtLimit(loggerMock.Object);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => context.Scripts.CallShared("AnySharedScript"));

        await Task.Yield();

        loggerMock.Verify(l => l.LogEventAsync(
            "script",
            "Error",
            InstanceName,
            It.IsAny<string>(),
            It.Is<string>(m => m.Contains("CallShared") && m.Contains("depth")),
            null),
            Times.Once);
    }

    [Fact]
    public async Task CallShared_RecursionLimitExceeded_NullLogger_DoesNotThrow()
    {
        var context = CreateContextAtLimit(siteEventLogger: null!);

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => context.Scripts.CallShared("AnySharedScript"));

        Assert.Contains("depth", ex.Message);
    }

    // -------------------------------------------------------------------------
    // Verify the event shape precisely (category, severity, message content)
    // -------------------------------------------------------------------------

    [Fact]
    public async Task CallScript_RecursionLimitExceeded_EventHasCorrectCategoryAndSeverity()
    {
        string? capturedCategory = null;
        string? capturedSeverity = null;
        string? capturedInstance = null;
        string? capturedSource = null;
        string? capturedMessage = null;

        var loggerMock = new Mock<ISiteEventLogger>();
        loggerMock
            .Setup(l => l.LogEventAsync(
                It.IsAny<string>(), It.IsAny<string>(),
                It.IsAny<string?>(), It.IsAny<string>(),
                It.IsAny<string>(), It.IsAny<string?>()))
            .Callback<string, string, string?, string, string, string?>(
                (cat, sev, inst, src, msg, det) =>
                {
                    capturedCategory = cat;
                    capturedSeverity = sev;
                    capturedInstance = inst;
                    capturedSource = src;
                    capturedMessage = msg;
                })
            .Returns(Task.CompletedTask);

        var context = CreateContextAtLimit(loggerMock.Object, maxCallDepth: 2);
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => context.CallScript("DeepScript"));
        await Task.Yield();

        Assert.Equal("script", capturedCategory);
        Assert.Equal("Error", capturedSeverity);
        Assert.Equal(InstanceName, capturedInstance);
        // Source fallback: no sourceScript wired, so the helper uses "InstanceScript:{instanceName}".
        Assert.Equal($"InstanceScript:{InstanceName}", capturedSource);
        Assert.NotNull(capturedMessage);
        Assert.Contains("CallScript", capturedMessage);
        Assert.Contains("2", capturedMessage);   // maxCallDepth in the message
    }
}
