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

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

/// <summary>
///     Unit tests for <see cref="EffectiveNameGuard"/> — the authoring-time UNS effective-name
///     uniqueness guard. Backed by an InMemory EF database seeded with one equipment carrying a
///     reference (override + backing raw tag), a VirtualTag, and a ScriptedAlarm.
/// </summary>
public sealed class EffectiveNameGuardTests
{
    private const string EquipmentId = "EQ-000000000001";

    // Reference REF-1 → raw tag TAG-1 (Name "speed"), NO override → effective "speed".
    // Reference REF-2 → raw tag TAG-2 (Name "raw-temp"), override "temperature" → effective "temperature".
    // VirtualTag VT-1 (Name "computed"). ScriptedAlarm AL-1 (Name "highpress").
    private const string RefNoOverrideId = "REF-1";
    private const string RefWithOverrideId = "REF-2";
    private const string VirtualTagId = "VT-1";
    private const string ScriptedAlarmId = "AL-1";

    private static IDbContextFactory<OtOpcUaConfigDbContext> SeedFactory()
    {
        var name = $"engname-{Guid.NewGuid():N}";
        var factory = new NamedFactory(name);
        using var db = factory.CreateDbContext();

        db.Tags.Add(new Tag
        {
            TagId = "TAG-1",
            DeviceId = "DEV-1",
            Name = "speed",
            DataType = "Float",
            AccessLevel = TagAccessLevel.Read,
            TagConfig = "{}",
        });
        db.Tags.Add(new Tag
        {
            TagId = "TAG-2",
            DeviceId = "DEV-1",
            Name = "raw-temp",
            DataType = "Float",
            AccessLevel = TagAccessLevel.Read,
            TagConfig = "{}",
        });

        db.UnsTagReferences.Add(new UnsTagReference
        {
            UnsTagReferenceId = RefNoOverrideId,
            EquipmentId = EquipmentId,
            TagId = "TAG-1",
            DisplayNameOverride = null, // effective = raw tag Name "speed"
        });
        db.UnsTagReferences.Add(new UnsTagReference
        {
            UnsTagReferenceId = RefWithOverrideId,
            EquipmentId = EquipmentId,
            TagId = "TAG-2",
            DisplayNameOverride = "temperature", // effective = "temperature"
        });

        db.VirtualTags.Add(new VirtualTag
        {
            VirtualTagId = VirtualTagId,
            EquipmentId = EquipmentId,
            Name = "computed",
            DataType = "Float",
            ScriptId = "SCR-1",
        });

        db.ScriptedAlarms.Add(new ScriptedAlarm
        {
            ScriptedAlarmId = ScriptedAlarmId,
            EquipmentId = EquipmentId,
            Name = "highpress",
            AlarmType = "AlarmCondition",
            MessageTemplate = "{TagPath} high",
            PredicateScriptId = "SCR-2",
        });

        db.SaveChanges();
        return factory;
    }

    private static EffectiveNameGuard Guard(IDbContextFactory<OtOpcUaConfigDbContext> factory) => new(factory);

    [Fact]
    public async Task Free_name_returns_null()
    {
        var guard = Guard(SeedFactory());

        var result = await guard.CheckAsync(
            EquipmentId, "brand-new-name", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);

        result.ShouldBeNull();
    }

    [Fact]
    public async Task Reference_vs_new_virtualtag_clash_returns_error_naming_the_reference()
    {
        var guard = Guard(SeedFactory());

        // A new VirtualTag proposing "speed" collides with reference REF-1's effective name.
        var result = await guard.CheckAsync(
            EquipmentId, "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);

        result.ShouldNotBeNull();
        result.ShouldBe($"effective name 'speed' already used by reference '{RefNoOverrideId}' in equipment '{EquipmentId}'");
    }

    [Fact]
    public async Task Override_effective_name_vs_new_reference_clash_returns_error()
    {
        var guard = Guard(SeedFactory());

        // A new reference proposing "temperature" collides with REF-2's OVERRIDE effective name.
        var result = await guard.CheckAsync(
            EquipmentId, "temperature", EffectiveNameSourceKind.Reference, excludeSourceId: null);

        result.ShouldNotBeNull();
        result.ShouldBe($"effective name 'temperature' already used by reference '{RefWithOverrideId}' in equipment '{EquipmentId}'");
    }

    [Fact]
    public async Task New_virtualtag_vs_scripted_alarm_clash_returns_error_naming_the_alarm()
    {
        var guard = Guard(SeedFactory());

        var result = await guard.CheckAsync(
            EquipmentId, "highpress", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);

        result.ShouldNotBeNull();
        result.ShouldBe($"effective name 'highpress' already used by ScriptedAlarm '{ScriptedAlarmId}' in equipment '{EquipmentId}'");
    }

    [Fact]
    public async Task Self_exclude_on_override_change_returns_null()
    {
        var guard = Guard(SeedFactory());

        // Editing REF-2 to change its override to "temperature" (its own current effective name)
        // must NOT collide with itself.
        var result = await guard.CheckAsync(
            EquipmentId, "temperature", EffectiveNameSourceKind.Reference, excludeSourceId: RefWithOverrideId);

        result.ShouldBeNull();
    }

    [Fact]
    public async Task Self_exclude_only_applies_to_same_kind()
    {
        var guard = Guard(SeedFactory());

        // Excluding a VirtualTag whose id happens to equal a reference id must NOT excuse the
        // reference collision — the self-row is identified by (kind, id) together.
        var result = await guard.CheckAsync(
            EquipmentId, "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: RefNoOverrideId);

        result.ShouldNotBeNull();
        result.ShouldBe($"effective name 'speed' already used by reference '{RefNoOverrideId}' in equipment '{EquipmentId}'");
    }

    [Fact]
    public async Task Ordinal_case_sensitivity_Speed_does_not_collide_with_speed()
    {
        var guard = Guard(SeedFactory());

        // "Speed" (capital S) must NOT collide with the existing "speed" (ordinal comparison).
        var result = await guard.CheckAsync(
            EquipmentId, "Speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);

        result.ShouldBeNull();
    }

    [Fact]
    public async Task Collision_is_scoped_to_the_equipment()
    {
        var guard = Guard(SeedFactory());

        // Same name, different equipment → no collision (per-equipment NodeId space).
        var result = await guard.CheckAsync(
            "EQ-000000000002", "speed", EffectiveNameSourceKind.VirtualTag, excludeSourceId: null);

        result.ShouldBeNull();
    }

    private sealed class NamedFactory(string name) : IDbContextFactory<OtOpcUaConfigDbContext>
    {
        public OtOpcUaConfigDbContext CreateDbContext() =>
            new(new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
                .UseInMemoryDatabase(name)
                .Options);

        public Task<OtOpcUaConfigDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default) =>
            Task.FromResult(CreateDbContext());
    }
}
