using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;

namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation;

/// <summary>
/// M9-T32b deploy-time validation: a dangling <c>{"$ref":"lib:Name"}</c> in any
/// validated script parameter/return schema must BLOCK deployment (a
/// <see cref="ValidationCategory.SchemaReference"/> error naming the missing ref).
/// A valid ref resolved through the supplied seam passes. The check is inert for
/// schemas with no <c>$ref</c> (existing behaviour unchanged) and when no resolver
/// is supplied and there are no refs.
/// </summary>
public class SchemaRefValidationTests
{
    private readonly ValidationService _sut = new();

    [Fact]
    public void Validate_DanglingRefInParameterDefinitions_BlocksDeploy()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "CallMe",
                    Code = "var x = 1;",
                    ParameterDefinitions = """{"$ref":"lib:MissingLib"}""",
                }
            ]
        };

        // Resolver knows nothing → the ref dangles.
        var result = _sut.Validate(config, resolveSchemaRef: _ => null);

        Assert.False(result.IsValid);
        var error = Assert.Single(result.Errors, e => e.Category == ValidationCategory.SchemaReference);
        Assert.Contains("MissingLib", error.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Validate_DanglingRefInReturnDefinition_BlocksDeploy()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "CallMe",
                    Code = "var x = 1;",
                    ReturnDefinition = """{"$ref":"lib:GoneReturn"}""",
                }
            ]
        };

        var result = _sut.Validate(config, resolveSchemaRef: _ => null);

        Assert.False(result.IsValid);
        Assert.Contains(result.Errors, e =>
            e.Category == ValidationCategory.SchemaReference
            && e.Message.Contains("GoneReturn", StringComparison.Ordinal));
    }

    [Fact]
    public void Validate_ValidRef_Passes()
    {
        const string libSchema = """{"type":"object","properties":{"qty":{"type":"integer"}}}""";
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "CallMe",
                    Code = "var x = 1;",
                    TriggerType = "Call",
                    ParameterDefinitions = """{"$ref":"lib:OrderRequest"}""",
                }
            ]
        };

        var result = _sut.Validate(
            config,
            resolveSchemaRef: name => name == "OrderRequest" ? libSchema : null);

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

    [Fact]
    public void Validate_NoRefSchema_NoSchemaReferenceError()
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "CallMe",
                    Code = "var x = 1;",
                    ParameterDefinitions = """{"type":"object","properties":{"a":{"type":"integer"}}}""",
                }
            ]
        };

        // Even with a resolver present, a schema with no $ref yields no ref errors.
        var result = _sut.Validate(config, resolveSchemaRef: _ => null);

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

    [Fact]
    public void Validate_NoResolverSupplied_NoRegression()
    {
        // The default deploy/design path with no resolver and no $ref schemas must
        // behave exactly as before (no SchemaReference errors).
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Attributes = [new ResolvedAttribute { CanonicalName = "Temp", Value = "25", DataType = "Double" }],
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "Monitor",
                    Code = "var x = 1;",
                    ParameterDefinitions = """{"type":"object","properties":{"a":{"type":"integer"}}}""",
                }
            ]
        };

        var result = _sut.Validate(config);

        Assert.True(result.IsValid);
        Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.SchemaReference);
    }

    [Fact]
    public void Validate_CyclicRef_BlocksDeployWithoutOverflow()
    {
        var schemas = new Dictionary<string, string>
        {
            ["A"] = """{"$ref":"lib:B"}""",
            ["B"] = """{"$ref":"lib:A"}""",
        };
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = "Instance1",
            Scripts =
            [
                new ResolvedScript
                {
                    CanonicalName = "CallMe",
                    Code = "var x = 1;",
                    ParameterDefinitions = """{"$ref":"lib:A"}""",
                }
            ]
        };

        var result = _sut.Validate(
            config,
            resolveSchemaRef: name => schemas.GetValueOrDefault(name));

        Assert.False(result.IsValid);
        Assert.Contains(result.Errors, e => e.Category == ValidationCategory.SchemaReference);
    }
}
