using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Types;

namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;

public class EquipmentScriptPathsTests
{
    // Effective-name → backing-tag RawPath map used across the substitution tests.
    private static readonly IReadOnlyDictionary<string, string> RefMap =
        new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["Speed"] = "Cell1/Modbus/Dev1/Speed",
            ["Out"] = "Cell1/Modbus/Dev1/Out",
            ["A"] = "Cell1/Modbus/Dev1/A",
            ["B"] = "Cell1/Modbus/Dev1/B",
            // an override-named reference: the effective name differs from the backing tag leaf name.
            ["MotorSpeed"] = "Cell1/Modbus/Dev1/raw_speed_01",
        };

    // ---- ExtractAlarmDependencyRefs (scripted-alarm dep graph; byte-parity seam) ----

    [Fact]
    public void ExtractAlarmDependencyRefs_predicate_reads_first_then_template_tokens_deduped()
    {
        // A template token identical to a predicate read appears once (predicate-first); a distinct
        // template token is appended after. This dedup/order is the parity contract both the composer
        // and the artifact decode rely on.
        EquipmentScriptPaths
            .ExtractAlarmDependencyRefs(
                predicateSource: "return ctx.GetTag(\"Mach.Temp\").Value > 90;",
                messageTemplate: "Temp {Mach.Temp} over {Mach.Limit}")
            .ShouldBe(["Mach.Temp", "Mach.Limit"]);
    }

    [Fact]
    public void ExtractAlarmDependencyRefs_excludes_reserved_equip_double_brace_token()
    {
        // The reserved {{equip}} double-brace form must NOT be picked up as a single-brace {TagPath}
        // token; only the genuine {Line.Temp} token is extracted.
        EquipmentScriptPaths
            .ExtractAlarmDependencyRefs(
                predicateSource: null,
                messageTemplate: "{{equip}} too hot: {Line.Temp}")
            .ShouldBe(["Line.Temp"]);
    }

    // ---- SubstituteEquipmentToken (v3 reference-relative, slash syntax) ----

    [Fact]
    public void SubstituteEquipmentToken_resolves_ref_to_rawpath_inside_GetTag()
    {
        EquipmentScriptPaths.SubstituteEquipmentToken(
                "ctx.GetTag(\"{{equip}}/Speed\")", RefMap)
            .ShouldBe("ctx.GetTag(\"Cell1/Modbus/Dev1/Speed\")");
    }

    [Fact]
    public void SubstituteEquipmentToken_resolves_ref_inside_SetVirtualTag()
    {
        EquipmentScriptPaths.SubstituteEquipmentToken(
                "ctx.SetVirtualTag(\"{{equip}}/Out\", x)", RefMap)
            .ShouldBe("ctx.SetVirtualTag(\"Cell1/Modbus/Dev1/Out\", x)");
    }

    [Fact]
    public void SubstituteEquipmentToken_override_named_ref_resolves_to_its_backing_rawpath()
    {
        // Effective name "MotorSpeed" (a DisplayNameOverride) resolves to the backing tag's RawPath whose
        // leaf name ("raw_speed_01") differs from the effective name — the override indirection.
        EquipmentScriptPaths.SubstituteEquipmentToken(
                "ctx.GetTag(\"{{equip}}/MotorSpeed\")", RefMap)
            .ShouldBe("ctx.GetTag(\"Cell1/Modbus/Dev1/raw_speed_01\")");
    }

    [Fact]
    public void SubstituteEquipmentToken_leaves_unresolved_ref_intact_without_throwing()
    {
        // An unknown reference name is left un-substituted (the validator rejects it first at deploy).
        var source = "ctx.GetTag(\"{{equip}}/Missing\")";
        EquipmentScriptPaths.SubstituteEquipmentToken(source, RefMap).ShouldBe(source);
    }

    [Fact]
    public void SubstituteEquipmentToken_leaves_comment_unchanged()
    {
        var source = "// uses {{equip}}/Speed here";
        EquipmentScriptPaths.SubstituteEquipmentToken(source, RefMap).ShouldBe(source);
    }

    [Fact]
    public void SubstituteEquipmentToken_leaves_logger_string_unchanged()
    {
        var source = "ctx.Logger.Information(\"{{equip}}/Speed\")";
        EquipmentScriptPaths.SubstituteEquipmentToken(source, RefMap).ShouldBe(source);
    }

    [Fact]
    public void SubstituteEquipmentToken_substitutes_all_occurrences()
    {
        var source = "ctx.GetTag(\"{{equip}}/A\"); ctx.GetTag(\"{{equip}}/B\");";
        var result = EquipmentScriptPaths.SubstituteEquipmentToken(source, RefMap);

        result.ShouldContain("ctx.GetTag(\"Cell1/Modbus/Dev1/A\")");
        result.ShouldContain("ctx.GetTag(\"Cell1/Modbus/Dev1/B\")");
        result.ShouldNotContain(EquipmentScriptPaths.EquipToken);
    }

    [Fact]
    public void SubstituteEquipmentToken_is_identity_when_map_empty()
    {
        var source = "ctx.GetTag(\"{{equip}}/Speed\")";
        EquipmentScriptPaths.SubstituteEquipmentToken(
                source, new Dictionary<string, string>(StringComparer.Ordinal))
            .ShouldBe(source);
    }

    [Fact]
    public void SubstituteEquipmentToken_is_identity_when_token_absent()
    {
        var source = "ctx.GetTag(\"Cell1/Modbus/Dev1/Speed\")";
        EquipmentScriptPaths.SubstituteEquipmentToken(source, RefMap).ShouldBe(source);
    }

    [Fact]
    public void SubstituteEquipmentToken_does_not_touch_raw_string_literal()
    {
        // Documents the regex limitation: only "double-quote" literals are handled,
        // matching the existing seam extractors. A raw-string literal is left as-is.
        var source = "ctx.GetTag(\"\"\"{{equip}}/Speed\"\"\")";
        EquipmentScriptPaths.SubstituteEquipmentToken(source, RefMap).ShouldBe(source);
    }

    // ---- ExtractEquipReferenceNames (script path-literal scoped) ----

    [Fact]
    public void ExtractEquipReferenceNames_returns_distinct_refs_first_seen()
    {
        var source = "ctx.GetTag(\"{{equip}}/Speed\"); ctx.SetVirtualTag(\"{{equip}}/Out\", x); ctx.GetTag(\"{{equip}}/Speed\");";
        EquipmentScriptPaths.ExtractEquipReferenceNames(source).ShouldBe(["Speed", "Out"]);
    }

    [Fact]
    public void ExtractEquipReferenceNames_ignores_token_in_comment_or_logger()
    {
        // Only ctx.GetTag/SetVirtualTag path literals are scanned — a token in a comment / logger string
        // is NOT reported (so it can never block a deploy).
        var source = "// {{equip}}/InComment\nctx.Logger.Information(\"{{equip}}/InLog\"); ctx.GetTag(\"{{equip}}/Real\");";
        EquipmentScriptPaths.ExtractEquipReferenceNames(source).ShouldBe(["Real"]);
    }

    [Fact]
    public void ExtractEquipReferenceNames_supports_ref_names_with_spaces()
    {
        // The whole post-prefix literal content is the reference name — so a space-bearing effective name
        // (valid raw name) is captured intact, matching what substitution resolves.
        EquipmentScriptPaths.ExtractEquipReferenceNames("ctx.GetTag(\"{{equip}}/My Tag\")")
            .ShouldBe(["My Tag"]);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("ctx.GetTag(\"Absolute/Path\")")]
    public void ExtractEquipReferenceNames_empty_when_no_token(string? source)
    {
        EquipmentScriptPaths.ExtractEquipReferenceNames(source).ShouldBeEmpty();
    }

    // ---- ExtractEquipReferenceNamesFromText (message-template free text) ----

    [Fact]
    public void ExtractEquipReferenceNamesFromText_captures_refs_bounded_by_whitespace()
    {
        EquipmentScriptPaths.ExtractEquipReferenceNamesFromText("Temp {{equip}}/Speed exceeded on {{equip}}/Limit")
            .ShouldBe(["Speed", "Limit"]);
    }

    [Fact]
    public void ExtractEquipReferenceNamesFromText_empty_when_no_token()
    {
        EquipmentScriptPaths.ExtractEquipReferenceNamesFromText("plain message {Foo.Bar}").ShouldBeEmpty();
    }

    // ---- ExtractDependencyRefs ----

    [Fact]
    public void ExtractDependencyRefs_returns_distinct_refs_in_first_seen_order()
    {
        var source = "ctx.GetTag(\"A\"); ctx.GetTag(\"B\"); ctx.GetTag(\"A\");";

        EquipmentScriptPaths.ExtractDependencyRefs(source)
            .ShouldBe(["A", "B"]);
    }

    [Fact]
    public void ExtractDependencyRefs_excludes_SetVirtualTag_writes()
    {
        var source = "ctx.GetTag(\"X\"); ctx.SetVirtualTag(\"Y\", 1);";

        EquipmentScriptPaths.ExtractDependencyRefs(source)
            .ShouldBe(["X"]);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void ExtractDependencyRefs_returns_empty_for_null_or_whitespace(string? source)
    {
        EquipmentScriptPaths.ExtractDependencyRefs(source).ShouldBeEmpty();
    }

    // ---- ContainsEquipToken ----

    [Fact]
    public void ContainsEquipToken_true_when_token_present()
    {
        EquipmentScriptPaths.ContainsEquipToken("ctx.GetTag(\"{{equip}}/X\")").ShouldBeTrue();
    }

    [Fact]
    public void ContainsEquipToken_false_when_token_absent()
    {
        EquipmentScriptPaths.ContainsEquipToken("ctx.GetTag(\"X\")").ShouldBeFalse();
    }

    [Fact]
    public void ContainsEquipToken_false_for_null()
    {
        EquipmentScriptPaths.ContainsEquipToken(null).ShouldBeFalse();
    }

    // ---- TryParseRelayBody ----

    [Theory]
    [InlineData("return ctx.GetTag(\"TestMachine_020.TestChangingInt\").Value;", "TestMachine_020.TestChangingInt")]
    [InlineData("  return   ctx . GetTag ( \"A.B\" ) . Value ; ", "A.B")]
    [InlineData("return ctx.GetTag(\"{{equip}}/Speed\").Value;", "{{equip}}/Speed")]
    public void TryParseRelayBody_accepts_exact_passthrough(string src, string expectedRef)
    {
        EquipmentScriptPaths.TryParseRelayBody(src, out var r).ShouldBeTrue();
        r.ShouldBe(expectedRef);
    }

    [Theory]
    [InlineData("return ctx.GetTag(\"A.B\").Value * 2;")]
    [InlineData("var x = ctx.GetTag(\"A.B\").Value; return x;")]
    [InlineData("return ctx.GetTag(\"A.B\").Value + ctx.GetTag(\"C.D\").Value;")]
    [InlineData("return ctx.GetTag(\"A.B\").Quality;")]
    [InlineData("return 5;")]
    [InlineData("")]
    public void TryParseRelayBody_rejects_non_relay(string src)
    {
        EquipmentScriptPaths.TryParseRelayBody(src, out var r).ShouldBeFalse();
        r.ShouldBeNull();
    }
}
