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>
/// Covers the B3-WP3 rename-warning extension of <see cref="RawTreeService"/>: the three warning kinds a
/// tag/ancestor rename can raise — (a) historized WITHOUT a <c>historianTagname</c> override (history
/// forks), (b) UNS-referenced (names the equipment), and (c) matched by a substring scan of a
/// <c>Script</c> body for the affected tag's OLD RawPath.
/// <para>
/// Seeds a bespoke InMemory slice (independent of the shared <c>RawTreeTestDb</c>) so each device holds
/// exactly one tag with a controlled property mix. The EF InMemory provider does not enforce
/// <c>RowVersion</c> tokens, so renames commit against the seeded row versions.
/// </para>
/// </summary>
[Trait("Category", "Unit")]
public sealed class RawTreeServiceRenameWarningTests
{
    private const string ClusterId = "MAIN";
    private const string FolderId = "RF-plant";
    private const string DriverId = "DRV-modbus";

    // Dev-all → the "everything" tag: historized (no override) + referenced + named in a script literal.
    private const string DevAllId = "DEV-all";
    private const string FlowTagId = "TAG-flow";
    private const string FlowRawPath = "Plant/modbus-1/Dev-all/flow";

    // Dev-pinned → a historized tag WITH a historianTagname override (pinned; must NOT warn).
    private const string DevPinnedId = "DEV-pinned";
    private const string TempTagId = "TAG-temp";

    // Dev-plain → a plain tag, unreferenced + absent from every script (must warn nothing).
    private const string DevPlainId = "DEV-plain";
    private const string IdleTagId = "TAG-idle";

    private const string EquipmentId = "EQ-000000000001";
    private const string EquipmentName = "packer-1";
    private const string ScriptName = "AreaCalc";

    private static (RawTreeService Service, string DbName) Seeded()
    {
        var name = $"rawwarn-{Guid.NewGuid():N}";
        using var db = CreateNamed(name);
        Seed(db);
        return (new RawTreeService(new NamedFactory(name)), name);
    }

    private static OtOpcUaConfigDbContext CreateNamed(string name) =>
        new(new DbContextOptionsBuilder<OtOpcUaConfigDbContext>().UseInMemoryDatabase(name).Options);

    private static byte[] DeviceRowVersion(string dbName, string deviceId)
    {
        using var db = CreateNamed(dbName);
        return db.Devices.Single(d => d.DeviceId == deviceId).RowVersion;
    }

    private static byte[] DriverRowVersion(string dbName, string driverId)
    {
        using var db = CreateNamed(dbName);
        return db.DriverInstances.Single(d => d.DriverInstanceId == driverId).RowVersion;
    }

    // ------------------------------------------------------------------------------------- Scenarios

    [Fact]
    public async Task Rename_of_ancestor_of_a_historized_referenced_scripted_tag_raises_all_three_warnings()
    {
        var (service, dbName) = Seeded();
        var rv = DeviceRowVersion(dbName, DevAllId);

        var result = await service.RenameDeviceAsync(DevAllId, "Dev-renamed", rv);

        result.Ok.ShouldBeTrue();
        result.Warnings.ShouldContain(w => w.Contains("historian")); // (a)
        result.Warnings.ShouldContain(w => w.Contains(EquipmentName)); // (b)
        result.Warnings.ShouldContain(w => w.Contains("script") && w.Contains(ScriptName)); // (c)
        result.Warnings.Count.ShouldBe(3);
    }

    [Fact]
    public async Task Rename_of_a_pinned_historized_tag_raises_no_historized_warning()
    {
        var (service, dbName) = Seeded();
        var rv = DeviceRowVersion(dbName, DevPinnedId);

        var result = await service.RenameDeviceAsync(DevPinnedId, "Dev-pinned2", rv);

        result.Ok.ShouldBeTrue();
        // A historianTagname override pins the tagname, so its history does NOT fork — no (a) warning; and
        // the pinned tag is neither referenced nor scripted, so nothing else fires either.
        result.Warnings.ShouldNotContain(w => w.Contains("historian"));
        result.Warnings.ShouldBeEmpty();
    }

    [Fact]
    public async Task Rename_of_an_unrelated_tags_ancestor_raises_no_warnings()
    {
        var (service, dbName) = Seeded();
        var rv = DeviceRowVersion(dbName, DevPlainId);

        var result = await service.RenameDeviceAsync(DevPlainId, "Dev-plain2", rv);

        result.Ok.ShouldBeTrue();
        result.Warnings.ShouldBeEmpty();
    }

    [Fact]
    public async Task Script_scan_matches_when_an_ancestor_is_renamed_and_the_old_path_is_in_a_script_body()
    {
        var (service, dbName) = Seeded();
        var rv = DriverRowVersion(dbName, DriverId);

        // Renaming the DRIVER (an ancestor of the scripted tag) still computes the tag's unchanged OLD
        // RawPath (rename not yet saved) and finds it as a substring of the script body.
        var result = await service.RenameDriverAsync(DriverId, "modbus-renamed", rv);

        result.Ok.ShouldBeTrue();
        result.Warnings.ShouldContain(w => w.Contains("script") && w.Contains(ScriptName));
    }

    // ------------------------------------------------------------------------------------------ Seed

    private static void Seed(OtOpcUaConfigDbContext db)
    {
        db.ServerClusters.Add(new ServerCluster
        {
            ClusterId = ClusterId,
            Name = "Main",
            Enterprise = "zb",
            Site = "warsaw-west",
            RedundancyMode = RedundancyMode.None,
            CreatedBy = "test",
        });

        db.RawFolders.Add(new RawFolder
        {
            RawFolderId = FolderId,
            ClusterId = ClusterId,
            ParentRawFolderId = null,
            Name = "Plant",
        });
        db.DriverInstances.Add(new DriverInstance
        {
            DriverInstanceId = DriverId,
            ClusterId = ClusterId,
            RawFolderId = FolderId,
            Name = "modbus-1",
            DriverType = "Modbus",
            DriverConfig = "{}",
        });

        db.Devices.Add(new Device { DeviceId = DevAllId, DriverInstanceId = DriverId, Name = "Dev-all", DeviceConfig = "{}" });
        db.Devices.Add(new Device { DeviceId = DevPinnedId, DriverInstanceId = DriverId, Name = "Dev-pinned", DeviceConfig = "{}" });
        db.Devices.Add(new Device { DeviceId = DevPlainId, DriverInstanceId = DriverId, Name = "Dev-plain", DeviceConfig = "{}" });

        // Historized, no historianTagname override → its default tagname is FlowRawPath, which moves.
        db.Tags.Add(new Tag
        {
            TagId = FlowTagId,
            DeviceId = DevAllId,
            TagGroupId = null,
            Name = "flow",
            DataType = "Float",
            AccessLevel = TagAccessLevel.Read,
            TagConfig = "{\"isHistorized\":true}",
        });
        // Historized WITH a historianTagname override → pinned, must not fork/warn.
        db.Tags.Add(new Tag
        {
            TagId = TempTagId,
            DeviceId = DevPinnedId,
            TagGroupId = null,
            Name = "temp",
            DataType = "Float",
            AccessLevel = TagAccessLevel.Read,
            TagConfig = "{\"isHistorized\":true,\"historianTagname\":\"Plant.Pinned.Temp\"}",
        });
        // Plain, unreferenced, absent from scripts.
        db.Tags.Add(new Tag
        {
            TagId = IdleTagId,
            DeviceId = DevPlainId,
            TagGroupId = null,
            Name = "idle",
            DataType = "Boolean",
            AccessLevel = TagAccessLevel.Read,
            TagConfig = "{}",
        });

        db.Equipment.Add(new Equipment
        {
            EquipmentId = EquipmentId,
            EquipmentUuid = Guid.NewGuid(),
            UnsLineId = "LINE-1",
            Name = EquipmentName,
            MachineCode = "packer_001",
        });
        db.UnsTagReferences.Add(new UnsTagReference
        {
            UnsTagReferenceId = "REF-flow",
            EquipmentId = EquipmentId,
            TagId = FlowTagId,
        });

        // A script whose body names the flow tag by its absolute RawPath literal.
        db.Scripts.Add(new Script
        {
            ScriptId = "SC-areacalc",
            Name = ScriptName,
            SourceCode = $"var v = ctx.GetTag(\"{FlowRawPath}\"); return v * 2;",
            SourceHash = "hash-areacalc",
        });

        db.SaveChanges();
    }

    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());
    }
}
