using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;

/// <summary>
/// v3 (M1): verifies the save-time equipment-relative-path guard on <see cref="UnsTreeService"/>. When a
/// VirtualTag binds a script that uses <c>{{equip}}/&lt;RefName&gt;</c>, every <c>&lt;RefName&gt;</c> must
/// resolve to one of the owning equipment's <c>UnsTagReference</c> effective names — otherwise the
/// create/update is rejected with an error naming the equipment and the unresolved token. This mirrors the
/// deploy gate (<c>DraftValidator.ValidateEquipReferenceResolution</c>): editor/authoring accept ⇔ publish accepts.
/// </summary>
[Trait("Category", "Unit")]
public sealed class VirtualTagEquipTokenValidationTests
{
    private const string EquipRefScript = "return ctx.GetTag(\"{{equip}}/Speed\").Value;";
    private const string PlainScript = "return ctx.GetTag(\"Cell1/Modbus/Dev1/Speed\").Value;";

    private static (UnsTreeService Service, string DbName) Fresh()
    {
        var dbName = $"uns-equiptoken-{Guid.NewGuid():N}";
        return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
    }

    /// <summary>
    /// Seeds an area→line→equipment path (equipment id <c>EQ-1</c>) plus one script whose source is
    /// <paramref name="scriptSource"/>, and — when <paramref name="referenceName"/> is set — a raw tag with
    /// that Name plus an <c>UnsTagReference</c> from EQ-1 to it (so the equipment has a reference whose
    /// effective name is <paramref name="referenceName"/>).
    /// </summary>
    private static void Seed(string dbName, string scriptSource, string? referenceName)
    {
        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = "MAIN", Name = "a" });
        db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" });
        db.Equipment.Add(new Equipment
        {
            EquipmentId = "EQ-1",
            EquipmentUuid = Guid.NewGuid(),
            UnsLineId = "LINE-1",
            Name = "TestMachine_001",
            MachineCode = "machine_001",
        });
        db.Scripts.Add(new Script
        {
            ScriptId = "SCRIPT-1",
            Name = "compute",
            SourceCode = scriptSource,
            SourceHash = "hash-1",
            Language = "CSharp",
        });
        if (referenceName is not null)
        {
            db.Tags.Add(new Tag
            {
                TagId = "TAG-1",
                DeviceId = "DEV-1",
                Name = referenceName,
                DataType = "Float",
                AccessLevel = TagAccessLevel.Read,
                TagConfig = "{}",
            });
            db.UnsTagReferences.Add(new UnsTagReference
            {
                UnsTagReferenceId = "REF-1",
                EquipmentId = "EQ-1",
                TagId = "TAG-1",
                DisplayNameOverride = null,
            });
        }
        db.SaveChanges();
    }

    private static VirtualTagInput Input(string virtualTagId = "VTAG-1", string name = "computed") =>
        new(virtualTagId, name, DataType: "Double", ScriptId: "SCRIPT-1",
            ChangeTriggered: true, TimerIntervalMs: null, Historize: false, Enabled: true);

    /// <summary>Seeds an existing VirtualTag and returns its RowVersion (for update-path tests).</summary>
    private static byte[] SeedVirtualTagAndRowVersion(string dbName)
    {
        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.VirtualTags.Add(new VirtualTag
        {
            VirtualTagId = "VTAG-1",
            EquipmentId = "EQ-1",
            Name = "computed",
            DataType = "Double",
            ScriptId = "SCRIPT-1",
        });
        db.SaveChanges();
        return db.VirtualTags.Single(v => v.VirtualTagId == "VTAG-1").RowVersion;
    }

    // ----- Create -----

    /// <summary>{{equip}}/Speed + a matching reference named "Speed" → create succeeds.</summary>
    [Fact]
    public async Task Create_equip_ref_that_resolves_succeeds()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, EquipRefScript, referenceName: "Speed");

        var result = await service.CreateVirtualTagAsync("EQ-1", Input());

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();
    }

    /// <summary>{{equip}}/Speed + no matching reference → create rejected, error names equipment + token.</summary>
    [Fact]
    public async Task Create_equip_ref_unresolved_rejected()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, EquipRefScript, referenceName: null);

        var result = await service.CreateVirtualTagAsync("EQ-1", Input());

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("EQ-1");
        result.Error.ShouldContain("{{equip}}/Speed");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.VirtualTags.Any(v => v.VirtualTagId == "VTAG-1").ShouldBeFalse();
    }

    /// <summary>A script with no {{equip}} token → create succeeds regardless of references.</summary>
    [Fact]
    public async Task Create_no_equip_token_succeeds_without_references()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, PlainScript, referenceName: null);

        var result = await service.CreateVirtualTagAsync("EQ-1", Input());

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();
    }

    // ----- Update -----

    /// <summary>{{equip}}/Speed + a matching reference → update succeeds.</summary>
    [Fact]
    public async Task Update_equip_ref_that_resolves_succeeds()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, EquipRefScript, referenceName: "Speed");
        var rv = SeedVirtualTagAndRowVersion(dbName);

        var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv);

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();
    }

    /// <summary>{{equip}}/Speed + no matching reference → update rejected, error names equipment + token.</summary>
    [Fact]
    public async Task Update_equip_ref_unresolved_rejected()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, EquipRefScript, referenceName: null);
        var rv = SeedVirtualTagAndRowVersion(dbName);

        var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv);

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("EQ-1");
        result.Error.ShouldContain("{{equip}}/Speed");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.VirtualTags.Single(v => v.VirtualTagId == "VTAG-1").Name.ShouldBe("computed");
    }

    /// <summary>A script with no {{equip}} token → update succeeds regardless of references.</summary>
    [Fact]
    public async Task Update_no_equip_token_succeeds_without_references()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, PlainScript, referenceName: null);
        var rv = SeedVirtualTagAndRowVersion(dbName);

        var result = await service.UpdateVirtualTagAsync("VTAG-1", Input(name: "renamed"), rv);

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();
    }

    // ----- ScriptedAlarm (Wave-B review MEDIUM-1: the invariant must hold on the SA surface too) -----

    private static ScriptedAlarmInput AlarmInput(string messageTemplate = "value out of range") =>
        new("ALM-1", "HighTemp", AlarmType: "Rate", Severity: 500, MessageTemplate: messageTemplate,
            PredicateScriptId: "SCRIPT-1", HistorizeToAveva: false, Retain: false, Enabled: true);

    /// <summary>SA predicate uses {{equip}}/Speed with no matching reference → create rejected, error names the token.</summary>
    [Fact]
    public async Task Create_alarm_equip_ref_in_predicate_unresolved_rejected()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, EquipRefScript, referenceName: null);

        var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput());

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("EQ-1");
        result.Error.ShouldContain("{{equip}}/Speed");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.ScriptedAlarms.Any(a => a.ScriptedAlarmId == "ALM-1").ShouldBeFalse();
    }

    /// <summary>SA message template carries {{equip}}/Bad with no matching reference → create rejected (template scan).</summary>
    [Fact]
    public async Task Create_alarm_equip_ref_in_message_template_unresolved_rejected()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, PlainScript, referenceName: "Speed"); // predicate has no token; only the template does

        var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput(messageTemplate: "bad {{equip}}/Missing here"));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("{{equip}}/Missing");
    }

    /// <summary>SA predicate {{equip}}/Speed with a matching reference → create succeeds.</summary>
    [Fact]
    public async Task Create_alarm_equip_ref_that_resolves_succeeds()
    {
        var (service, dbName) = Fresh();
        Seed(dbName, EquipRefScript, referenceName: "Speed");

        var result = await service.CreateScriptedAlarmAsync("EQ-1", AlarmInput());

        result.Ok.ShouldBeTrue();
        result.Error.ShouldBeNull();
    }
}
