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

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

/// <summary>
/// Verifies the equipment-bound VirtualTag CRUD mutations on <see cref="UnsTreeService"/>, including
/// the equipment-existence guard, the script-chosen guard, the change-or-timer trigger rule, the
/// 50 ms timer minimum, duplicate-id / duplicate-name guards, and the script-candidate loader.
/// </summary>
/// <remarks>
/// The EF InMemory provider enforces neither <c>RowVersion</c> concurrency nor the DbContext CHECK
/// constraints, so the <c>DbUpdateConcurrencyException</c> branches are not exercised here by design,
/// and the service's own trigger/timer guards are what protect the data in these tests.
/// </remarks>
[Trait("Category", "Unit")]
public sealed class UnsTreeServiceVirtualTagTests
{
    private static (UnsTreeService Service, string DbName) Fresh()
    {
        var dbName = $"uns-vtag-{Guid.NewGuid():N}";
        return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
    }

    /// <summary>
    /// Seeds an area→line→equipment path (equipment id <c>EQ-1</c>) plus a single script
    /// (<c>SCRIPT-1</c>, language CSharp) so the create/update paths have a valid script to bind.
    /// </summary>
    private static void SeedEquipmentAndScript(string dbName)
    {
        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 = "machine-1",
            MachineCode = "machine_001",
        });
        db.Scripts.Add(new Script
        {
            ScriptId = "SCRIPT-1",
            Name = "compute speed",
            SourceCode = "return 1;",
            SourceHash = "hash-1",
            Language = "CSharp",
        });
        db.SaveChanges();
    }

    private static VirtualTagInput Input(
        string virtualTagId,
        string name,
        string scriptId = "SCRIPT-1",
        bool changeTriggered = true,
        int? timerIntervalMs = null) =>
        new(virtualTagId, name, DataType: "Double", scriptId,
            changeTriggered, timerIntervalMs, Historize: false, Enabled: true);

    // ----- CreateVirtualTag -----

    /// <summary>A valid change-triggered virtual tag persists with EquipmentId set.</summary>
    [Fact]
    public async Task CreateVirtualTag_persists()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);

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

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

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        var vtag = db.VirtualTags.Single(v => v.VirtualTagId == "VTAG-1");
        vtag.EquipmentId.ShouldBe("EQ-1");
        vtag.Name.ShouldBe("computed");
        vtag.DataType.ShouldBe("Double");
        vtag.ScriptId.ShouldBe("SCRIPT-1");
        vtag.ChangeTriggered.ShouldBeTrue();
    }

    /// <summary>Creating a virtual tag for an equipment id that does not exist is blocked.</summary>
    [Fact]
    public async Task CreateVirtualTag_equipment_not_found_blocked()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);

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

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("Equipment 'EQ-NOPE' not found.");

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

    /// <summary>A virtual tag with neither change-trigger nor a timer is blocked.</summary>
    [Fact]
    public async Task CreateVirtualTag_no_trigger_blocked()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);

        var result = await service.CreateVirtualTagAsync(
            "EQ-1", Input("VTAG-1", "computed", changeTriggered: false, timerIntervalMs: null));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("Pick at least one trigger — change or timer.");

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

    /// <summary>A virtual tag with a timer below the 50 ms minimum is blocked.</summary>
    [Fact]
    public async Task CreateVirtualTag_timer_below_50_blocked()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);

        var result = await service.CreateVirtualTagAsync(
            "EQ-1", Input("VTAG-1", "computed", changeTriggered: false, timerIntervalMs: 10));

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("TimerIntervalMs must be at least 50 ms.");

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

    /// <summary>Creating a virtual tag with a VirtualTagId that already exists is blocked.</summary>
    [Fact]
    public async Task CreateVirtualTag_duplicate_id_blocked()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);
        await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));

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

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("VirtualTag 'VTAG-1' already exists.");
    }

    /// <summary>Creating a virtual tag whose Name already exists on the same equipment is blocked.</summary>
    [Fact]
    public async Task CreateVirtualTag_duplicate_name_on_equipment_blocked()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);
        await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));

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

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("A virtual tag named 'computed' already exists on this equipment.");
    }

    /// <summary>Creating a virtual tag with no script chosen is blocked.</summary>
    [Fact]
    public async Task CreateVirtualTag_no_script_blocked()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);

        var input = Input("VTAG-1", "computed", scriptId: "", changeTriggered: true);
        var result = await service.CreateVirtualTagAsync("EQ-1", input);

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("Pick a script");

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

    // ----- UpdateVirtualTag -----

    /// <summary>Updating a virtual tag changes its mutable fields and keeps EquipmentId.</summary>
    [Fact]
    public async Task UpdateVirtualTag_changes_fields()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);
        await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.VirtualTags.Single(v => v.VirtualTagId == "VTAG-1").RowVersion;
        }

        var updated = new VirtualTagInput("VTAG-1", "renamed", DataType: "Int32", ScriptId: "SCRIPT-1",
            ChangeTriggered: false, TimerIntervalMs: 250, Historize: true, Enabled: false);

        var result = await service.UpdateVirtualTagAsync("VTAG-1", updated, rv);

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

        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        var after = verify.VirtualTags.Single(v => v.VirtualTagId == "VTAG-1");
        after.Name.ShouldBe("renamed");
        after.DataType.ShouldBe("Int32");
        after.ChangeTriggered.ShouldBeFalse();
        after.TimerIntervalMs.ShouldBe(250);
        after.Historize.ShouldBeTrue();
        after.Enabled.ShouldBeFalse();
        after.EquipmentId.ShouldBe("EQ-1");
    }

    /// <summary>Updating a virtual tag that no longer exists returns the row-gone error.</summary>
    [Fact]
    public async Task UpdateVirtualTag_missing_row_returns_error()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);

        var result = await service.UpdateVirtualTagAsync("VTAG-nope", Input("VTAG-nope", "x"), []);

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("Row no longer exists.");
    }

    /// <summary>Renaming a virtual tag to a name already used by another tag on the same equipment is blocked.</summary>
    [Fact]
    public async Task UpdateVirtualTag_duplicate_name_on_equipment_blocked()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);
        await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-A", "vt_a"));
        await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-B", "vt_b"));

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.VirtualTags.Single(v => v.VirtualTagId == "VTAG-A").RowVersion;
        }

        var input = Input("VTAG-A", "vt_b"); // try to rename to the name already used by VTAG-B
        var result = await service.UpdateVirtualTagAsync("VTAG-A", input, rv);

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("already exists on this equipment");
    }

    // ----- DeleteVirtualTag -----

    /// <summary>Deleting a virtual tag removes the row.</summary>
    [Fact]
    public async Task DeleteVirtualTag_removes_row()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);
        await service.CreateVirtualTagAsync("EQ-1", Input("VTAG-1", "computed"));

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.VirtualTags.Single(v => v.VirtualTagId == "VTAG-1").RowVersion;
        }

        var result = await service.DeleteVirtualTagAsync("VTAG-1", rv);

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

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

    /// <summary>Deleting a virtual tag that is already gone is a no-op success.</summary>
    [Fact]
    public async Task DeleteVirtualTag_already_gone_returns_ok()
    {
        var (service, _) = Fresh();

        var result = await service.DeleteVirtualTagAsync("VTAG-ghost", []);

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

    // ----- LoadScriptsAsync -----

    /// <summary>The script loader returns the seeded scripts projected to (id, "Name (Language)").</summary>
    [Fact]
    public async Task LoadScripts_returns_scripts()
    {
        var (service, dbName) = Fresh();
        SeedEquipmentAndScript(dbName);

        var scripts = await service.LoadScriptsAsync();

        scripts.Count.ShouldBe(1);
        scripts[0].ScriptId.ShouldBe("SCRIPT-1");
        scripts[0].Display.ShouldBe("compute speed (CSharp)");
    }
}
