using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;

namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;

/// <summary>
///     Covers the <see cref="ScriptSandbox.Build"/> argument-validation guards. The
///     guards are the only call-site protection against a typo / mis-wired context
///     type silently producing a sandbox missing the concrete context's assembly
///     reference; without coverage, the guards could be deleted by a refactor without
///     any test failing. (Core.Scripting-011.)
/// </summary>
[Trait("Category", "Unit")]
public sealed class ScriptSandboxBuildTests
{
    /// <summary>Verifies that a null context type throws ArgumentNullException.</summary>
    [Fact]
    public void Null_context_type_throws_ArgumentNullException()
    {
        Should.Throw<ArgumentNullException>(() => ScriptSandbox.Build(null!));
    }

    /// <summary>Verifies that a non-ScriptContext type throws ArgumentException.</summary>
    [Fact]
    public void Non_ScriptContext_type_throws_ArgumentException()
    {
        // ScriptSandbox must reject types that don't derive from ScriptContext —
        // ScriptGlobals<TContext> is constrained where TContext : ScriptContext, so
        // sneaking a non-derived type past Build would later blow up inside Roslyn.
        Should.Throw<ArgumentException>(() => ScriptSandbox.Build(typeof(string)))
              .ParamName.ShouldBe("contextType");
    }

    /// <summary>Verifies that the abstract ScriptContext base type is accepted by the sandbox builder.</summary>
    [Fact]
    public void Abstract_ScriptContext_base_type_is_accepted()
    {
        // The base ScriptContext type satisfies the IsAssignableFrom check, so the
        // factory must not reject it even though it cannot be instantiated directly.
        // Callers wiring a base-typed sandbox up for diagnostics rely on this.
        var options = ScriptSandbox.Build(typeof(ScriptContext));
        options.ShouldNotBeNull();
    }

    /// <summary>Verifies that a concrete subclass is accepted and its assembly is referenced in the sandbox.</summary>
    [Fact]
    public void Concrete_subclass_is_accepted_and_its_assembly_referenced()
    {
        // The concrete context type's assembly must end up in the allow-listed
        // references — otherwise Roslyn cannot resolve ScriptGlobals<TContext> at
        // compile. We can't easily inspect the ScriptOptions metadata references
        // by-assembly cross-version, so we exercise the end-to-end path instead: a
        // script compiled against FakeScriptContext must successfully reach a
        // FakeScriptContext member.
        var evaluator = ScriptEvaluator<FakeScriptContext, double>.Compile(
            """return (double)ctx.GetTag("X").Value;""");
        evaluator.ShouldNotBeNull();
    }
}
