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

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

/// <summary>
/// Covers <see cref="RawTreeService"/> — the B2-WP1 Raw-tree data layer: lazy children per level,
/// create/rename/delete/move happy paths and guards (sibling-name collision, non-empty-container delete
/// blocks naming the blocker, referenced-tag delete blocks naming the equipment), and rename warnings.
/// <para>
/// As with the <c>UnsTreeService</c> suite, the EF InMemory provider does not enforce <c>RowVersion</c>
/// concurrency tokens, so the <c>DbUpdateConcurrencyException</c> branch is not directly exercised; the
/// concurrent-modification failure is instead covered via the "row vanished under a stale handle" path
/// (another actor deletes the row before the rename commits).
/// </para>
/// </summary>
[Trait("Category", "Unit")]
public sealed class RawTreeServiceTests
{
    private static (RawTreeService Service, string DbName) Seeded()
    {
        var name = RawTreeTestDb.SeedFresh();
        return (new RawTreeService(RawTreeTestDb.Factory(name)), name);
    }

    private static byte[] RowVersionOf(
        string dbName, Func<ZB.MOM.WW.OtOpcUa.Configuration.OtOpcUaConfigDbContext, byte[]> selector)
    {
        using var db = RawTreeTestDb.CreateNamed(dbName);
        return selector(db);
    }

    // ----------------------------------------------------------------------------------- Read layer

    [Fact]
    public async Task LoadRoots_yields_enterprise_and_cluster_with_lazy_flag_and_root_count()
    {
        var (service, _) = Seeded();

        var roots = await service.LoadRootsAsync();

        var ent = roots.ShouldHaveSingleItem();
        ent.Kind.ShouldBe(RawNodeKind.Enterprise);
        ent.DisplayName.ShouldBe("zb");

        var cluster = ent.Children.ShouldHaveSingleItem();
        cluster.Kind.ShouldBe(RawNodeKind.Cluster);
        cluster.EntityId.ShouldBe(RawTreeTestDb.ClusterId);
        cluster.HasLazyChildren.ShouldBeTrue();
        cluster.Children.ShouldBeEmpty(); // clusters load lazily
        cluster.ChildCount.ShouldBe(2); // 1 root folder + 1 cluster-root driver
    }

    [Fact]
    public async Task LoadChildren_of_cluster_yields_root_folders_and_root_drivers()
    {
        var (service, _) = Seeded();
        var cluster = (await service.LoadRootsAsync()).Single().Children.Single();

        var children = await service.LoadChildrenAsync(cluster);

        var folder = children.Single(n => n.Kind == RawNodeKind.Folder);
        folder.DisplayName.ShouldBe("PLCs");
        folder.EntityId.ShouldBe(RawTreeTestDb.RootFolderId);
        folder.HasLazyChildren.ShouldBeTrue();
        folder.ChildCount.ShouldBe(1); // the foldered driver

        var driver = children.Single(n => n.Kind == RawNodeKind.Driver);
        driver.DisplayName.ShouldBe("s7-1");
        driver.DriverType.ShouldBe("S7");
        driver.ChildCount.ShouldBe(1); // its default device
    }

    [Fact]
    public async Task LoadChildren_propagates_driver_type_down_to_device_group_and_tag()
    {
        var (service, _) = Seeded();
        var cluster = (await service.LoadRootsAsync()).Single().Children.Single();
        var folder = (await service.LoadChildrenAsync(cluster)).Single(n => n.Kind == RawNodeKind.Folder);

        var driver = (await service.LoadChildrenAsync(folder)).ShouldHaveSingleItem();
        driver.Kind.ShouldBe(RawNodeKind.Driver);
        driver.DriverType.ShouldBe("Modbus");

        var device = (await service.LoadChildrenAsync(driver)).ShouldHaveSingleItem();
        device.Kind.ShouldBe(RawNodeKind.Device);
        device.DriverType.ShouldBe("Modbus"); // propagated
        device.ChildCount.ShouldBe(1); // one root group (tags live under the group)

        var group = (await service.LoadChildrenAsync(device)).ShouldHaveSingleItem();
        group.Kind.ShouldBe(RawNodeKind.TagGroup);
        group.DriverType.ShouldBe("Modbus"); // propagated
        group.ChildCount.ShouldBe(2); // two tags

        var tags = await service.LoadChildrenAsync(group);
        tags.Count.ShouldBe(2);
        tags.ShouldAllBe(t => t.Kind == RawNodeKind.Tag);
        tags.ShouldAllBe(t => !t.HasLazyChildren);
        tags.ShouldAllBe(t => t.DriverType == "Modbus"); // propagated
        (await service.LoadChildrenAsync(tags[0])).ShouldBeEmpty(); // leaf
    }

    // -------------------------------------------------------------------------------- Create/happy

    [Fact]
    public async Task CreateFolder_then_CreateDriver_autocreates_default_device()
    {
        var (service, dbName) = Seeded();

        var folder = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Cell-B");
        folder.Ok.ShouldBeTrue();
        folder.CreatedId.ShouldNotBeNull();

        var driver = await service.CreateDriverAsync(RawTreeTestDb.ClusterId, folder.CreatedId, "abcip-1", "AbCip", "{}");
        driver.Ok.ShouldBeTrue();

        using var db = RawTreeTestDb.CreateNamed(dbName);
        db.Devices.Count(d => d.DriverInstanceId == driver.CreatedId).ShouldBe(1); // default device auto-created
    }

    [Fact]
    public async Task CreateFolder_rejects_sibling_name_collision()
    {
        var (service, _) = Seeded();

        var dup = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "PLCs"); // already a root folder
        dup.Ok.ShouldBeFalse();
        dup.Error!.ShouldContain("already exists");
    }

    [Fact]
    public async Task CreateFolder_rejects_invalid_segment()
    {
        var (service, _) = Seeded();

        var bad = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "a/b");
        bad.Ok.ShouldBeFalse();
        bad.Error!.ShouldContain("/");
    }

    [Fact]
    public async Task CreateTagGroup_rejects_sibling_collision()
    {
        var (service, _) = Seeded();

        var dup = await service.CreateTagGroupAsync(RawTreeTestDb.FolderedDeviceId, null, "Motors");
        dup.Ok.ShouldBeFalse();
        dup.Error!.ShouldContain("already exists");
    }

    // ------------------------------------------------------------------------------- Rename + warn

    [Fact]
    public async Task RenameFolder_warns_about_historized_tag_beneath()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == RawTreeTestDb.RootFolderId).RowVersion);

        var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "PLC", rv);

        result.Ok.ShouldBeTrue();
        result.Warnings.ShouldContain(w => w.Contains("historized"));
    }

    [Fact]
    public async Task RenameDriver_warns_about_equipment_reference_beneath()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.RootDriverId).RowVersion);

        var result = await service.RenameDriverAsync(RawTreeTestDb.RootDriverId, "s7-main", rv);

        result.Ok.ShouldBeTrue();
        result.Warnings.ShouldContain(w => w.Contains(RawTreeTestDb.EquipmentName));
    }

    [Fact]
    public async Task RenameFolder_rejects_sibling_collision()
    {
        var (service, dbName) = Seeded();
        await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Spare");
        var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == RawTreeTestDb.RootFolderId).RowVersion);

        var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "Spare", rv);

        result.Ok.ShouldBeFalse();
        result.Error!.ShouldContain("already exists");
    }

    // -------------------------------------------------------------------------------- Delete blocks

    [Fact]
    public async Task DeleteFolder_blocked_by_contained_driver_names_the_blocker()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == RawTreeTestDb.RootFolderId).RowVersion);

        var result = await service.DeleteFolderAsync(RawTreeTestDb.RootFolderId, rv);

        result.Ok.ShouldBeFalse();
        result.Error!.ShouldContain("driver");
        result.Error!.ShouldContain("PLCs");
    }

    [Fact]
    public async Task DeleteDevice_blocked_by_tags()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.Devices.Single(d => d.DeviceId == RawTreeTestDb.RootDeviceId).RowVersion);

        var result = await service.DeleteDeviceAsync(RawTreeTestDb.RootDeviceId, rv);

        result.Ok.ShouldBeFalse();
        result.Error!.ShouldContain("tags");
    }

    [Fact]
    public async Task DeleteTagGroup_blocked_by_tags()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.TagGroups.Single(g => g.TagGroupId == RawTreeTestDb.RootGroupId).RowVersion);

        var result = await service.DeleteTagGroupAsync(RawTreeTestDb.RootGroupId, rv);

        result.Ok.ShouldBeFalse();
        result.Error!.ShouldContain("tags");
    }

    [Fact]
    public async Task DeleteDriver_blocked_when_a_device_holds_tags_names_the_device()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.RootDriverId).RowVersion);

        var result = await service.DeleteDriverAsync(RawTreeTestDb.RootDriverId, rv);

        result.Ok.ShouldBeFalse();
        result.Error!.ShouldContain("Device1"); // the offending device is named
    }

    [Fact]
    public async Task DeleteTag_blocked_by_reference_names_the_equipment()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.ReferencedTagId).RowVersion);

        var result = await service.DeleteTagAsync(RawTreeTestDb.ReferencedTagId, rv);

        result.Ok.ShouldBeFalse();
        result.Error!.ShouldContain(RawTreeTestDb.EquipmentName);
        result.Error!.ShouldContain(RawTreeTestDb.EquipmentId);
    }

    // -------------------------------------------------------------------------------- Delete happy

    [Fact]
    public async Task DeleteTag_succeeds_when_unreferenced()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.Tags.Single(t => t.TagId == RawTreeTestDb.PlainTagId).RowVersion);

        var result = await service.DeleteTagAsync(RawTreeTestDb.PlainTagId, rv);

        result.Ok.ShouldBeTrue();
        using var db = RawTreeTestDb.CreateNamed(dbName);
        db.Tags.Any(t => t.TagId == RawTreeTestDb.PlainTagId).ShouldBeFalse();
    }

    [Fact]
    public async Task DeleteFolder_succeeds_when_empty()
    {
        var (service, dbName) = Seeded();
        var created = await service.CreateFolderAsync(RawTreeTestDb.ClusterId, null, "Empty");
        var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == created.CreatedId!).RowVersion);

        var result = await service.DeleteFolderAsync(created.CreatedId!, rv);

        result.Ok.ShouldBeTrue();
    }

    // ------------------------------------------------------------------------------------ Move

    [Fact]
    public async Task MoveDriverToFolder_reparents_the_driver()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.RootDriverId).RowVersion);

        var result = await service.MoveDriverToFolderAsync(RawTreeTestDb.RootDriverId, RawTreeTestDb.RootFolderId, rv);

        result.Ok.ShouldBeTrue();
        using var db = RawTreeTestDb.CreateNamed(dbName);
        db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.RootDriverId)
            .RawFolderId.ShouldBe(RawTreeTestDb.RootFolderId);
    }

    [Fact]
    public async Task MoveDriverToFolder_to_cluster_root_clears_the_folder()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId).RowVersion);

        var result = await service.MoveDriverToFolderAsync(RawTreeTestDb.FolderedDriverId, null, rv);

        result.Ok.ShouldBeTrue();
        using var db = RawTreeTestDb.CreateNamed(dbName);
        db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId)
            .RawFolderId.ShouldBeNull();
    }

    // -------------------------------------------------------------------------- Concurrency / edit

    [Fact]
    public async Task RenameFolder_surfaces_failure_when_row_vanished_concurrently()
    {
        var (service, dbName) = Seeded();
        var rv = RowVersionOf(dbName, db => db.RawFolders.Single(f => f.RawFolderId == RawTreeTestDb.RootFolderId).RowVersion);

        // Simulate another actor deleting the folder's contents + the folder itself out from under us.
        using (var db = RawTreeTestDb.CreateNamed(dbName))
        {
            db.RawFolders.Remove(db.RawFolders.Single(f => f.RawFolderId == RawTreeTestDb.RootFolderId));
            db.SaveChanges();
        }

        var result = await service.RenameFolderAsync(RawTreeTestDb.RootFolderId, "Whatever", rv);

        result.Ok.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Warnings.ShouldBeEmpty();
    }

    [Fact]
    public async Task LoadTagForEdit_returns_projection()
    {
        var (service, _) = Seeded();

        var dto = await service.LoadTagForEditAsync(RawTreeTestDb.HistorizedTagId);

        dto.ShouldNotBeNull();
        dto.Value.Name.ShouldBe("speed");
        dto.Value.DeviceId.ShouldBe(RawTreeTestDb.FolderedDeviceId);
        dto.Value.TagGroupId.ShouldBe(RawTreeTestDb.RootGroupId);
        dto.Value.DataType.ShouldBe("Float");
    }
}
