using ScadaLink.Commons.Types.Flattening;
using ScadaLink.TemplateEngine.Validation;

namespace ScadaLink.TemplateEngine.Tests.Validation;

public class SemanticValidatorTests
{
    private readonly SemanticValidator _sut = new();

    [Fact]
    public void Validate_CallScriptTargetNotFound_ReturnsError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "Caller",
                    Code = "CallScript(\"NonExistent\");"
                }
            ]
        };

        var result = _sut.Validate(config);
        Assert.Contains(result.Errors, e =>
            e.Category == ValidationCategory.CallTargetNotFound &&
            e.Message.Contains("NonExistent"));
    }

    [Fact]
    public void Validate_CallScriptTargetExists_NoError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript { CanonicalName = "Target", Code = "var x = 1;" },
                new ResolvedScript { CanonicalName = "Caller", Code = "CallScript(\"Target\");" }
            ]
        };

        var result = _sut.Validate(config);
        Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound);
    }

    [Fact]
    public void Validate_CallSharedTargetNotFound_ReturnsError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "Caller",
                    Code = "CallShared(\"MissingShared\");"
                }
            ]
        };

        var result = _sut.Validate(config, sharedScripts: []);
        Assert.Contains(result.Errors, e =>
            e.Category == ValidationCategory.CallTargetNotFound &&
            e.Message.Contains("MissingShared"));
    }

    [Fact]
    public void Validate_CallSharedTargetExists_NoError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript { CanonicalName = "Caller", Code = "CallShared(\"Utility\");" }
            ]
        };

        var shared = new List<ResolvedScript>
        {
            new() { CanonicalName = "Utility", Code = "// shared" }
        };

        var result = _sut.Validate(config, shared);
        Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound);
    }

    [Fact]
    public void Validate_ParameterCountMismatch_ReturnsError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "Target",
                    Code = "var x = 1;",
                    ParameterDefinitions = "[{\"name\":\"a\",\"type\":\"Int32\"},{\"name\":\"b\",\"type\":\"String\"}]"
                },
                new ResolvedScript
                {
                    CanonicalName = "Caller",
                    Code = "CallScript(\"Target\", 42);" // 1 arg but 2 expected
                }
            ]
        };

        var result = _sut.Validate(config);
        Assert.Contains(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
    }

    [Fact]
    public void Validate_RangeViolationOnNonNumeric_ReturnsError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "Status", DataType = "String" }
            ],
            Alarms =
            [
                new ResolvedAlarm
                {
                    CanonicalName = "BadAlarm",
                    TriggerType = "RangeViolation",
                    TriggerConfiguration = "{\"attributeName\":\"Status\"}"
                }
            ]
        };

        var result = _sut.Validate(config);
        Assert.Contains(result.Errors, e => e.Category == ValidationCategory.TriggerOperandType);
    }

    [Fact]
    public void Validate_RangeViolationOnNumeric_NoError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "Temp", DataType = "Double" }
            ],
            Alarms =
            [
                new ResolvedAlarm
                {
                    CanonicalName = "HighTemp",
                    TriggerType = "RangeViolation",
                    TriggerConfiguration = "{\"attributeName\":\"Temp\"}"
                }
            ]
        };

        var result = _sut.Validate(config);
        Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.TriggerOperandType);
    }

    [Fact]
    public void Validate_OnTriggerScriptNotFound_ReturnsError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts = [new ResolvedScript { CanonicalName = "OtherScript", Code = "var x = 1;" }],
            Alarms =
            [
                new ResolvedAlarm
                {
                    CanonicalName = "Alarm1",
                    TriggerType = "ValueMatch",
                    OnTriggerScriptCanonicalName = "MissingScript"
                }
            ]
        };

        var result = _sut.Validate(config);
        Assert.Contains(result.Errors, e => e.Category == ValidationCategory.OnTriggerScriptNotFound);
    }

    [Fact]
    public void Validate_InstanceScriptCallsAlarmOnTrigger_ReturnsError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript { CanonicalName = "AlarmHandler", Code = "// alarm handler" },
                new ResolvedScript
                {
                    CanonicalName = "RegularScript",
                    Code = "CallScript(\"AlarmHandler\");"
                }
            ],
            Alarms =
            [
                new ResolvedAlarm
                {
                    CanonicalName = "Alarm1",
                    TriggerType = "ValueMatch",
                    OnTriggerScriptCanonicalName = "AlarmHandler"
                }
            ]
        };

        var result = _sut.Validate(config);
        Assert.Contains(result.Errors, e => e.Category == ValidationCategory.CrossCallViolation);
    }

    [Fact]
    public void ExtractCallTargets_MultipleCallTypes()
    {
        var code = @"
            var x = CallScript(""Script1"", arg1, arg2);
            CallShared(""Shared1"");
            CallScript(""Script2"");
        ";

        var targets = SemanticValidator.ExtractCallTargets(code);

        Assert.Equal(3, targets.Count);
        Assert.Contains(targets, t => t.TargetName == "Script1" && !t.IsShared && t.ArgumentCount == 2);
        Assert.Contains(targets, t => t.TargetName == "Shared1" && t.IsShared && t.ArgumentCount == 0);
        Assert.Contains(targets, t => t.TargetName == "Script2" && !t.IsShared && t.ArgumentCount == 0);
    }

    [Fact]
    public void ParseParameterDefinitions_ValidJson_ReturnsList()
    {
        var json = "[{\"name\":\"a\",\"type\":\"Int32\"},{\"name\":\"b\",\"type\":\"String\"}]";
        var result = SemanticValidator.ParseParameterDefinitions(json);

        Assert.Equal(2, result.Count);
        Assert.Equal("Int32", result[0]);
        Assert.Equal("String", result[1]);
    }

    [Fact]
    public void ParseParameterDefinitions_NullOrEmpty_ReturnsEmpty()
    {
        Assert.Empty(SemanticValidator.ParseParameterDefinitions(null));
        Assert.Empty(SemanticValidator.ParseParameterDefinitions(""));
    }

    // ── HiLo validation ─────────────────────────────────────────────────────

    private static FlattenedConfiguration HiLoConfig(string attrName, string dataType, string triggerJson) =>
        new()
        {
            InstanceUniqueName = "Instance1",
            Attributes = [new ResolvedAttribute { CanonicalName = attrName, DataType = dataType }],
            Alarms =
            [
                new ResolvedAlarm
                {
                    CanonicalName = "Hi/Lo Alarm",
                    TriggerType = "HiLo",
                    TriggerConfiguration = triggerJson
                }
            ]
        };

    [Fact]
    public void Validate_HiLoOnNonNumericAttribute_ReturnsError()
    {
        var config = HiLoConfig("Status", "String",
            "{\"attributeName\":\"Status\",\"hi\":80}");

        var result = _sut.Validate(config);

        Assert.Contains(result.Errors,
            e => e.Category == ValidationCategory.TriggerOperandType
              && e.Message.Contains("HiLo")
              && e.Message.Contains("non-numeric"));
    }

    [Fact]
    public void Validate_HiLoOnNumericAttribute_NoOperandTypeError()
    {
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\",\"hi\":80,\"hiHi\":100}");

        var result = _sut.Validate(config);

        Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.TriggerOperandType);
    }

    [Fact]
    public void Validate_HiLoNoSetpoints_ReturnsWarning()
    {
        // No setpoints means the alarm can never fire — design-time warning.
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\"}");

        var result = _sut.Validate(config);

        Assert.Contains(result.Warnings,
            w => w.Category == ValidationCategory.TriggerOperandType
              && w.Message.Contains("no setpoints"));
    }

    [Fact]
    public void Validate_HiLoLoLoGreaterThanLo_ReturnsError()
    {
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\",\"loLo\":20,\"lo\":10}");

        var result = _sut.Validate(config);

        Assert.Contains(result.Errors,
            e => e.Category == ValidationCategory.TriggerOperandType
              && e.Message.Contains("LoLo")
              && e.Message.Contains("Lo"));
    }

    [Fact]
    public void Validate_HiLoHiGreaterThanHiHi_ReturnsError()
    {
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\",\"hi\":120,\"hiHi\":100}");

        var result = _sut.Validate(config);

        Assert.Contains(result.Errors,
            e => e.Category == ValidationCategory.TriggerOperandType
              && e.Message.Contains("Hi")
              && e.Message.Contains("HiHi"));
    }

    [Fact]
    public void Validate_HiLoLowSideOverlapsHighSide_ReturnsError()
    {
        // Lo (50) >= Hi (40) — bands overlap.
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\",\"lo\":50,\"hi\":40}");

        var result = _sut.Validate(config);

        Assert.Contains(result.Errors,
            e => e.Category == ValidationCategory.TriggerOperandType
              && e.Message.Contains("overlap"));
    }

    [Fact]
    public void Validate_HiLoOnlyHighSideConfigured_NoOrderingError()
    {
        // Only Hi/HiHi configured — no low-side comparison needed.
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\",\"hi\":80,\"hiHi\":100}");

        var result = _sut.Validate(config);

        Assert.DoesNotContain(result.Errors,
            e => e.Category == ValidationCategory.TriggerOperandType);
    }

    [Fact]
    public void Validate_HiLoNegativeDeadband_ReturnsError()
    {
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\",\"hi\":80,\"hiHi\":100,\"hiDeadband\":-1}");

        var result = _sut.Validate(config);

        Assert.Contains(result.Errors,
            e => e.Category == ValidationCategory.TriggerOperandType
              && e.Message.Contains("Hi deadband")
              && e.Message.Contains("non-negative"));
    }

    [Fact]
    public void Validate_HiLoZeroDeadband_NoError()
    {
        // Zero deadband is the default (no hysteresis) and must be accepted.
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\",\"hi\":80,\"hiHi\":100,\"hiDeadband\":0,\"hiHiDeadband\":0}");

        var result = _sut.Validate(config);

        Assert.DoesNotContain(result.Errors,
            e => e.Category == ValidationCategory.TriggerOperandType);
    }

    [Fact]
    public void Validate_HiLoValidOrdering_NoErrors()
    {
        // LoLo (-10) < Lo (0) < Hi (90) < HiHi (100) — fully valid.
        var config = HiLoConfig("Temp", "Double",
            "{\"attributeName\":\"Temp\",\"loLo\":-10,\"lo\":0,\"hi\":90,\"hiHi\":100}");

        var result = _sut.Validate(config);

        Assert.DoesNotContain(result.Errors,
            e => e.Category == ValidationCategory.TriggerOperandType);
        Assert.DoesNotContain(result.Warnings,
            w => w.Category == ValidationCategory.TriggerOperandType);
    }
}
