using System.Collections;
using System.Reflection;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Events;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Host.Engines;

namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;

/// <summary>
/// F8b — verifies <see cref="RoslynVirtualTagEvaluator"/> compiles user expressions through
/// the Core.Scripting sandbox, runs them against the dependency dictionary, caches the
/// compiled assembly per source, and surfaces failures (compile error, sandbox violation,
/// runtime throw) as <c>VirtualTagEvalResult.Failure</c> instead of propagating exceptions.
/// </summary>
public sealed class RoslynVirtualTagEvaluatorTests
{
    /// <summary>Captures published <see cref="ScriptLogEntry"/> records for assertion.</summary>
    private sealed class FakePublisher : IScriptLogPublisher
    {
        /// <summary>Gets the entries published so far.</summary>
        public List<ScriptLogEntry> Published { get; } = [];

        /// <inheritdoc/>
        public void Publish(ScriptLogEntry entry) => Published.Add(entry);
    }

    /// <summary>Builds a no-op <see cref="ScriptRootLogger"/> for tests that don't assert on logging.</summary>
    private static ScriptRootLogger NoOpScriptRoot() =>
        new(new LoggerConfiguration().CreateLogger());

    /// <summary>Verifies that simple addition expression is evaluated correctly.</summary>
    [Fact]
    public void Evaluate_simple_addition_returns_summed_value()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        var result = sut.Evaluate(
            virtualTagId: "vt-sum",
            expression: "return (int)ctx.GetTag(\"a\").Value + (int)ctx.GetTag(\"b\").Value;",
            dependencies: new Dictionary<string, object?> { ["a"] = 10, ["b"] = 32 });

        result.Success.ShouldBeTrue(result.Reason);
        result.Value.ShouldBe(42);
    }

    /// <summary>Verifies that compiled expressions are cached across multiple calls.</summary>
    [Fact]
    public void Evaluate_caches_compiled_expression_across_calls()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());
        const string expr = "return (int)ctx.GetTag(\"x\").Value * 2;";

        var first = sut.Evaluate("vt-cache", expr, new Dictionary<string, object?> { ["x"] = 5 });
        var second = sut.Evaluate("vt-cache", expr, new Dictionary<string, object?> { ["x"] = 7 });

        first.Success.ShouldBeTrue(first.Reason);
        first.Value.ShouldBe(10);
        second.Success.ShouldBeTrue(second.Reason);
        second.Value.ShouldBe(14);
    }

    /// <summary>Verifies that compile errors return Failure with a descriptive reason.</summary>
    [Fact]
    public void Evaluate_compile_error_returns_Failure_with_reason()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        var result = sut.Evaluate("vt-bad", "this is not valid C#;", new Dictionary<string, object?>());

        result.Success.ShouldBeFalse();
        result.Reason.ShouldNotBeNull();
        result.Reason.ShouldContain("compile");
    }

    /// <summary>Verifies that runtime exceptions return Failure with a descriptive reason.</summary>
    [Fact]
    public void Evaluate_runtime_exception_returns_Failure_with_reason()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        var result = sut.Evaluate(
            virtualTagId: "vt-div0",
            expression: "int a = 0; return 1 / a;",
            dependencies: new Dictionary<string, object?>());

        result.Success.ShouldBeFalse();
        result.Reason.ShouldNotBeNull();
        result.Reason.ShouldContain("threw");
    }

    /// <summary>Verifies that empty expressions return Failure.</summary>
    [Fact]
    public void Evaluate_empty_expression_returns_Failure()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        sut.Evaluate("vt-empty", "", new Dictionary<string, object?>()).Success.ShouldBeFalse();
        sut.Evaluate("vt-empty", "   ", new Dictionary<string, object?>()).Success.ShouldBeFalse();
    }

    /// <summary>Verifies that evaluation after disposal returns Failure.</summary>
    [Fact]
    public void Evaluate_after_dispose_returns_Failure()
    {
        var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());
        sut.Dispose();

        var result = sut.Evaluate("vt", "return 1;", new Dictionary<string, object?>());

        result.Success.ShouldBeFalse();
        result.Reason!.ShouldContain("disposed");
    }

    // ── A — passthrough fast-path: the mirror shape `return ctx.GetTag("X").Value;`
    //    is answered directly from the dependency value, skipping Roslyn compilation. ──

    /// <summary>Mirror passthrough returns the dependency value verbatim without compiling.</summary>
    [Fact]
    public void Passthrough_returns_dependency_value_without_compiling()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        var result = sut.Evaluate(
            virtualTagId: "vt-mirror",
            expression: "return ctx.GetTag(\"a\").Value;",
            dependencies: new Dictionary<string, object?> { ["a"] = 42 });

        result.Success.ShouldBeTrue(result.Reason);
        result.Value.ShouldBe(42);
    }

    /// <summary>The passthrough fast-path returns the raw object reference, not a re-boxed copy —
    /// proof the value flowed straight through without round-tripping a Roslyn script run.</summary>
    [Fact]
    public void Passthrough_returns_same_object_reference_as_dependency()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());
        var payload = new object();

        var result = sut.Evaluate(
            virtualTagId: "vt-ref",
            expression: "return ctx.GetTag(\"a\").Value;",
            dependencies: new Dictionary<string, object?> { ["a"] = payload });

        result.Success.ShouldBeTrue(result.Reason);
        result.Value.ShouldBeSameAs(payload);
    }

    /// <summary>Whitespace-tolerant mirror shapes still take the passthrough fast-path.</summary>
    [Fact]
    public void Passthrough_whitespace_variants_match()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        var result = sut.Evaluate(
            virtualTagId: "vt-ws",
            expression: "  return  ctx . GetTag( \"a\" ) . Value ; ",
            dependencies: new Dictionary<string, object?> { ["a"] = 7 });

        result.Success.ShouldBeTrue(result.Reason);
        result.Value.ShouldBe(7);
    }

    /// <summary>A near-miss (arithmetic on the mirror value) falls through to Roslyn and still works.</summary>
    [Fact]
    public void Non_passthrough_falls_through_to_Roslyn()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        var result = sut.Evaluate(
            virtualTagId: "vt-plus1",
            expression: "return (int)ctx.GetTag(\"a\").Value + 1;",
            dependencies: new Dictionary<string, object?> { ["a"] = 42 });

        result.Success.ShouldBeTrue(result.Reason);
        result.Value.ShouldBe(43);
    }

    /// <summary>Passthrough with an absent dependency yields the SAME result the Roslyn path
    /// produces: <c>GetTag</c> returns a Bad snapshot whose <c>.Value</c> is null, so the script
    /// returns null and the evaluator wraps it as <c>Ok(null)</c> (success, null value).</summary>
    [Fact]
    public void Passthrough_missing_dependency_matches_Roslyn_behaviour()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());
        const string mirror = "return ctx.GetTag(\"missing\").Value;";

        // Roslyn baseline: same source, but force a near-miss that compiles, to capture the
        // not-found semantics independently. Here we just assert the mirror's own missing-dep
        // result equals the documented Ok(null) shape.
        var passthrough = sut.Evaluate("vt-miss", mirror, new Dictionary<string, object?>());

        passthrough.Success.ShouldBeTrue(passthrough.Reason);
        passthrough.Value.ShouldBeNull();
        passthrough.Reason.ShouldBeNull();
    }

    /// <summary>Cross-check: the equivalent Roslyn-compiled read of a missing dependency
    /// produces exactly the same <c>Ok(null)</c> result, proving the fast-path is byte-identical.</summary>
    [Fact]
    public void Roslyn_missing_dependency_also_returns_Ok_null()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());
        // `(object?)...Value` forces the compiled path (not the mirror shape) but reads the same
        // missing tag; result must match the passthrough missing-dep result above.
        var result = sut.Evaluate(
            "vt-miss-roslyn",
            "return (object?)ctx.GetTag(\"missing\").Value;",
            new Dictionary<string, object?>());

        result.Success.ShouldBeTrue(result.Reason);
        result.Value.ShouldBeNull();
        result.Reason.ShouldBeNull();
    }

    /// <summary>Direct parity test: the EXACT mirror source <c>return ctx.GetTag("missing").Value;</c>
    /// evaluated against an empty dependency dictionary takes the fast-path and yields
    /// <c>Ok(null)</c>; a minimally-altered but semantically-identical non-passthrough variant
    /// <c>return (object?)ctx.GetTag("missing").Value;</c> compiled by Roslyn against the same
    /// empty deps yields the identical <c>Ok(null)</c> — proving the fast-path is byte-identical
    /// to the Roslyn path for the missing-dependency case.</summary>
    [Fact]
    public void Passthrough_exact_mirror_missing_dep_matches_Roslyn_baseline()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        // Fast-path: exact mirror shape → passthrough returns Ok(null) for missing dep.
        var fastPath = sut.Evaluate(
            "vt-parity-fast",
            "return ctx.GetTag(\"missing\").Value;",
            new Dictionary<string, object?>());

        fastPath.Success.ShouldBeTrue(fastPath.Reason);
        fastPath.Value.ShouldBeNull();
        fastPath.Reason.ShouldBeNull();

        // Roslyn path: `(object?)` cast forces compilation but reads the same missing tag.
        var roslynPath = sut.Evaluate(
            "vt-parity-roslyn",
            "return (object?)ctx.GetTag(\"missing\").Value;",
            new Dictionary<string, object?>());

        roslynPath.Success.ShouldBeTrue(roslynPath.Reason);
        roslynPath.Value.ShouldBeNull();
        roslynPath.Reason.ShouldBeNull();
    }

    /// <summary>Decisive proof the fast-path skips Roslyn: the compiled-script cache (which every
    /// Roslyn evaluation populates via <c>GetOrAdd</c>) stays EMPTY after a mirror evaluation, then
    /// grows to one entry once a genuine (non-mirror) expression forces compilation.</summary>
    [Fact]
    public void Passthrough_does_not_populate_the_compiled_script_cache()
    {
        using var sut = new RoslynVirtualTagEvaluator(NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot());

        // Mirror shape — must take the fast-path, leaving the Roslyn cache untouched.
        sut.Evaluate("vt-mirror", "return ctx.GetTag(\"a\").Value;",
            new Dictionary<string, object?> { ["a"] = 1 });
        CompiledCacheCount(sut).ShouldBe(0);

        // Non-mirror shape — must compile, populating exactly one cache entry.
        sut.Evaluate("vt-real", "return (int)ctx.GetTag(\"a\").Value + 1;",
            new Dictionary<string, object?> { ["a"] = 1 });
        CompiledCacheCount(sut).ShouldBe(1);
    }

    /// <summary>
    /// A script's <c>ctx.Logger.*</c> call flows through the injected root script logger and out
    /// the <see cref="ScriptLogTopicSink"/>, producing exactly one <see cref="ScriptLogEntry"/>
    /// carrying the message and the bound identity (<c>ScriptId</c> + <c>VirtualTagId</c> both
    /// equal the virtual-tag id in the live path).
    /// </summary>
    [Fact]
    public void Script_logger_call_publishes_entry_with_bound_identity()
    {
        var publisher = new FakePublisher();
        var root = new LoggerConfiguration()
            .MinimumLevel.Verbose()
            .WriteTo.Sink(new ScriptLogTopicSink(publisher, LogEventLevel.Information))
            .CreateLogger();
        using var sut = new RoslynVirtualTagEvaluator(
            NullLogger<RoslynVirtualTagEvaluator>.Instance, new ScriptRootLogger(root));

        var result = sut.Evaluate(
            virtualTagId: "vt-log",
            expression: "ctx.Logger.Information(\"hi from script\"); return 1;",
            dependencies: new Dictionary<string, object?>());

        result.Success.ShouldBeTrue(result.Reason);
        publisher.Published.Count.ShouldBe(1);
        var entry = publisher.Published[0];
        entry.Message.ShouldBe("hi from script");
        entry.ScriptId.ShouldBe("vt-log");
        entry.VirtualTagId.ShouldBe("vt-log");
    }

    /// <summary>Reads the count of the private compiled-script cache via reflection.</summary>
    private static int CompiledCacheCount(RoslynVirtualTagEvaluator sut)
    {
        var field = typeof(RoslynVirtualTagEvaluator)
            .GetField("_cache", BindingFlags.Instance | BindingFlags.NonPublic);
        field.ShouldNotBeNull();
        var cache = (ICollection)field.GetValue(sut)!;
        return cache.Count;
    }
}
