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

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

/// <summary>
///     Exercises the AST walker that extracts static tag dependencies from user scripts
///     + rejects every form of non-literal path. Locks the parse shape the virtual-tag
///     engine's change-trigger scheduler will depend on (Phase 7 plan Stream A.2).
/// </summary>
[Trait("Category", "Unit")]
public sealed class DependencyExtractorTests
{
    [Fact]
    public void Extracts_single_literal_read()
    {
        var result = DependencyExtractor.Extract(
            """return ctx.GetTag("Line1/Speed").Value;""");

        result.IsValid.ShouldBeTrue();
        result.Reads.ShouldContain("Line1/Speed");
        result.Writes.ShouldBeEmpty();
        result.Rejections.ShouldBeEmpty();
    }

    [Fact]
    public void Extracts_multiple_distinct_reads()
    {
        var result = DependencyExtractor.Extract(
            """
            var a = ctx.GetTag("Line1/A").Value;
            var b = ctx.GetTag("Line1/B").Value;
            return (double)a + (double)b;
            """);
        result.IsValid.ShouldBeTrue();
        result.Reads.Count.ShouldBe(2);
        result.Reads.ShouldContain("Line1/A");
        result.Reads.ShouldContain("Line1/B");
    }

    [Fact]
    public void Deduplicates_identical_reads_across_the_script()
    {
        var result = DependencyExtractor.Extract(
            """
            if (((double)ctx.GetTag("X").Value) > 0)
                return ctx.GetTag("X").Value;
            return 0;
            """);
        result.IsValid.ShouldBeTrue();
        result.Reads.Count.ShouldBe(1);
        result.Reads.ShouldContain("X");
    }

    [Fact]
    public void Tracks_virtual_tag_writes_separately_from_reads()
    {
        var result = DependencyExtractor.Extract(
            """
            var v = (double)ctx.GetTag("InTag").Value;
            ctx.SetVirtualTag("OutTag", v * 2);
            return v;
            """);
        result.IsValid.ShouldBeTrue();
        result.Reads.ShouldContain("InTag");
        result.Writes.ShouldContain("OutTag");
        result.Reads.ShouldNotContain("OutTag");
        result.Writes.ShouldNotContain("InTag");
    }

    [Fact]
    public void Rejects_variable_path()
    {
        var result = DependencyExtractor.Extract(
            """
            var path = "Line1/Speed";
            return ctx.GetTag(path).Value;
            """);
        result.IsValid.ShouldBeFalse();
        result.Rejections.Count.ShouldBe(1);
        result.Rejections[0].Message.ShouldContain("string literal");
    }

    [Fact]
    public void Rejects_concatenated_path()
    {
        var result = DependencyExtractor.Extract(
            """return ctx.GetTag("Line1/" + "Speed").Value;""");
        result.IsValid.ShouldBeFalse();
        result.Rejections[0].Message.ShouldContain("string literal");
    }

    [Fact]
    public void Rejects_interpolated_path()
    {
        var result = DependencyExtractor.Extract(
            """
            var n = 1;
            return ctx.GetTag($"Line{n}/Speed").Value;
            """);
        result.IsValid.ShouldBeFalse();
        result.Rejections[0].Message.ShouldContain("string literal");
    }

    [Fact]
    public void Rejects_method_returned_path()
    {
        var result = DependencyExtractor.Extract(
            """
            string BuildPath() => "Line1/Speed";
            return ctx.GetTag(BuildPath()).Value;
            """);
        result.IsValid.ShouldBeFalse();
        result.Rejections[0].Message.ShouldContain("string literal");
    }

    [Fact]
    public void Rejects_empty_literal_path()
    {
        var result = DependencyExtractor.Extract(
            """return ctx.GetTag("").Value;""");
        result.IsValid.ShouldBeFalse();
        result.Rejections[0].Message.ShouldContain("empty");
    }

    [Fact]
    public void Rejects_whitespace_only_path()
    {
        var result = DependencyExtractor.Extract(
            """return ctx.GetTag("   ").Value;""");
        result.IsValid.ShouldBeFalse();
    }

    [Fact]
    public void Ignores_non_ctx_method_named_GetTag()
    {
        // Scripts are free to define their own helper called "GetTag" — as long as it's
        // not on the ctx instance, the extractor doesn't pick it up. The sandbox
        // compile will still reject any path that isn't on the ScriptContext type.
        var result = DependencyExtractor.Extract(
            """
            string helper_GetTag(string p) => p;
            return helper_GetTag("NotATag");
            """);
        result.IsValid.ShouldBeTrue();
        result.Reads.ShouldBeEmpty();
    }

    [Fact]
    public void Empty_source_is_a_no_op()
    {
        DependencyExtractor.Extract("").IsValid.ShouldBeTrue();
        DependencyExtractor.Extract("   ").IsValid.ShouldBeTrue();
        DependencyExtractor.Extract(null!).IsValid.ShouldBeTrue();
    }

    [Fact]
    public void Rejection_carries_source_span_for_UI_pointing()
    {
        // Offending path at column 23-29 in the source — Admin UI uses Span to
        // underline the exact token.
        const string src = """return ctx.GetTag(path).Value;""";
        var result = DependencyExtractor.Extract(src);
        result.IsValid.ShouldBeFalse();
        result.Rejections[0].Span.Start.ShouldBeGreaterThan(0);
        result.Rejections[0].Span.Length.ShouldBeGreaterThan(0);
    }

    [Fact]
    public void Multiple_bad_paths_all_reported_in_one_pass()
    {
        var result = DependencyExtractor.Extract(
            """
            var p1 = "A"; var p2 = "B";
            return ctx.GetTag(p1).Value.ToString() + ctx.GetTag(p2).Value.ToString();
            """);
        result.IsValid.ShouldBeFalse();
        result.Rejections.Count.ShouldBe(2);
    }

    [Fact]
    public void Nested_literal_GetTag_inside_expression_is_extracted()
    {
        // Supports patterns like ctx.GetTag("A") > ctx.GetTag("B") — both literal args
        // are captured even when the enclosing expression is complex.
        var result = DependencyExtractor.Extract(
            """
            return ((double)ctx.GetTag("A").Value) > ((double)ctx.GetTag("B").Value);
            """);
        result.IsValid.ShouldBeTrue();
        result.Reads.Count.ShouldBe(2);
    }
}
