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 Area/Line CRUD mutations on <see cref="UnsTreeService"/>, including the
/// decision-#122 area-reassignment guard that blocks moving an area to a different cluster
/// when its driver-bound equipment would be orphaned from its driver's cluster.
/// </summary>
/// <remarks>
/// The EF InMemory provider does not enforce <c>RowVersion</c> concurrency, so the
/// <c>DbUpdateConcurrencyException</c> branches are not exercised here by design.
/// </remarks>
[Trait("Category", "Unit")]
public sealed class UnsTreeServiceAreaLineTests
{
    private static (UnsTreeService Service, string DbName) Fresh()
    {
        var dbName = $"uns-crud-{Guid.NewGuid():N}";
        return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
    }

    // ----- CreateArea -----

    /// <summary>A new area is persisted and visible on a fresh context.</summary>
    [Fact]
    public async Task CreateArea_then_load_shows_it()
    {
        var (service, dbName) = Fresh();

        var result = await service.CreateAreaAsync("MAIN", "AREA-NEW", "assembly", "  ");

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

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        var area = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-NEW");
        area.ClusterId.ShouldBe("MAIN");
        area.Name.ShouldBe("assembly");
        area.Notes.ShouldBeNull(); // whitespace-only notes collapse to null
    }

    /// <summary>Creating an area whose id already exists returns the duplicate error.</summary>
    [Fact]
    public async Task CreateArea_duplicate_id_returns_error()
    {
        var (service, dbName) = Fresh();
        await service.CreateAreaAsync("MAIN", "AREA-DUP", "first", null);

        var result = await service.CreateAreaAsync("MAIN", "AREA-DUP", "second", null);

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("Area 'AREA-DUP' already exists.");

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        db.UnsAreas.Single(a => a.UnsAreaId == "AREA-DUP").Name.ShouldBe("first");
    }

    // ----- UpdateArea -----

    /// <summary>Updating an area changes its name and notes (notes whitespace collapses to null).</summary>
    [Fact]
    public async Task UpdateArea_changes_name_and_notes()
    {
        var (service, dbName) = Fresh();
        await service.CreateAreaAsync("MAIN", "AREA-1", "old", "old notes");

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-1").RowVersion;
        }

        var result = await service.UpdateAreaAsync("AREA-1", "new", "  ", "MAIN", rv);

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

        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        var area = verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-1");
        area.Name.ShouldBe("new");
        area.Notes.ShouldBeNull();
        area.ClusterId.ShouldBe("MAIN");
    }

    /// <summary>
    /// v3 Batch-1: the decision-#122 driver-orphan guard was RETIRED — equipment no longer binds a
    /// driver (<c>Equipment.DriverInstanceId</c> is gone), so a cluster move can never orphan
    /// driver-bound equipment. The former blocked/allowed guard tests are replaced by this one, which
    /// proves a cross-cluster area reassignment now succeeds unconditionally (equipment under the area
    /// rides along). The equipment↔driver cluster invariant is re-covered by the Batch-2 Raw-tree tests.
    /// </summary>
    [Fact]
    public async Task UpdateArea_reassign_cluster_now_allowed_guard_retired()
    {
        var (service, dbName) = Fresh();

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-X", ClusterId = "MAIN", Name = "a" });
            db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-X", UnsAreaId = "AREA-X", Name = "l" });
            db.Equipment.Add(new Equipment
            {
                EquipmentId = "EQ-BOUND",
                EquipmentUuid = Guid.NewGuid(),
                UnsLineId = "LINE-X",
                Name = "m",
                MachineCode = "machine_x",
            });
            db.SaveChanges();
            rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").RowVersion;
        }

        var result = await service.UpdateAreaAsync("AREA-X", "a", null, "SITE-A", rv);

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

        // The area moved to SITE-A.
        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").ClusterId.ShouldBe("SITE-A");
    }

    /// <summary>Updating an area that no longer exists returns the row-gone error.</summary>
    [Fact]
    public async Task UpdateArea_missing_row_returns_error()
    {
        var (service, _) = Fresh();

        var result = await service.UpdateAreaAsync("NOPE", "n", null, "MAIN", []);

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

    // ----- DeleteArea -----

    /// <summary>Deleting an area with no lines removes the row.</summary>
    [Fact]
    public async Task DeleteArea_removes_row()
    {
        var (service, dbName) = Fresh();
        await service.CreateAreaAsync("MAIN", "AREA-DEL", "a", null);

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-DEL").RowVersion;
        }

        var result = await service.DeleteAreaAsync("AREA-DEL", rv);

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

        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        verify.UnsAreas.Any(a => a.UnsAreaId == "AREA-DEL").ShouldBeFalse();
    }

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

        var result = await service.DeleteAreaAsync("GHOST", []);

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

    // ----- CreateLine -----

    /// <summary>A new line is persisted under its area.</summary>
    [Fact]
    public async Task CreateLine_then_load_shows_it()
    {
        var (service, dbName) = Fresh();
        await service.CreateAreaAsync("MAIN", "AREA-1", "a", null);

        var result = await service.CreateLineAsync("AREA-1", "LINE-NEW", "line-a", "  ");

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

        using var db = UnsTreeTestDb.CreateNamed(dbName);
        var line = db.UnsLines.Single(l => l.UnsLineId == "LINE-NEW");
        line.UnsAreaId.ShouldBe("AREA-1");
        line.Name.ShouldBe("line-a");
        line.Notes.ShouldBeNull();
    }

    /// <summary>Creating a line whose id already exists returns the duplicate error.</summary>
    [Fact]
    public async Task CreateLine_duplicate_id_returns_error()
    {
        var (service, _) = Fresh();
        await service.CreateLineAsync("AREA-1", "LINE-DUP", "first", null);

        var result = await service.CreateLineAsync("AREA-1", "LINE-DUP", "second", null);

        result.Ok.ShouldBeFalse();
        result.Error.ShouldBe("Line 'LINE-DUP' already exists.");
    }

    // ----- UpdateLine -----

    /// <summary>Updating a line moves it to a new area and changes its name and notes.</summary>
    [Fact]
    public async Task UpdateLine_changes_area_name_and_notes()
    {
        var (service, dbName) = Fresh();
        await service.CreateLineAsync("AREA-1", "LINE-1", "old", "old notes");

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-1").RowVersion;
        }

        var result = await service.UpdateLineAsync("LINE-1", "new", "  ", "AREA-2", rv);

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

        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        var line = verify.UnsLines.Single(l => l.UnsLineId == "LINE-1");
        line.UnsAreaId.ShouldBe("AREA-2");
        line.Name.ShouldBe("new");
        line.Notes.ShouldBeNull();
    }

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

        var result = await service.UpdateLineAsync("NOPE", "n", null, "AREA-1", []);

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

    /// <summary>
    /// v3 Batch-1: the decision-#122 driver-orphan guard was RETIRED (equipment no longer binds a
    /// driver). The former blocked/allowed line-reparent guard tests are replaced by this one, which
    /// proves reparenting a line to an area in a different cluster now succeeds unconditionally.
    /// </summary>
    [Fact]
    public async Task UpdateLine_reparent_to_other_cluster_now_allowed_guard_retired()
    {
        var (service, dbName) = Fresh();

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            db.UnsAreas.Add(new UnsArea { UnsAreaId = "A1-MAIN", ClusterId = "MAIN", Name = "area-main" });
            db.UnsAreas.Add(new UnsArea { UnsAreaId = "A2-SITE-A", ClusterId = "SITE-A", Name = "area-site-a" });
            db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-BOUND", UnsAreaId = "A1-MAIN", Name = "line" });
            db.Equipment.Add(new Equipment
            {
                EquipmentId = "EQ-LINE-BOUND",
                EquipmentUuid = Guid.NewGuid(),
                UnsLineId = "LINE-BOUND",
                Name = "eq",
                MachineCode = "mc_line_bound",
            });
            db.SaveChanges();
            rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").RowVersion;
        }

        var result = await service.UpdateLineAsync("LINE-BOUND", "line", null, "A2-SITE-A", rv);

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

        // The line moved to the target area.
        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        verify.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").UnsAreaId.ShouldBe("A2-SITE-A");
    }

    // ----- DeleteLine -----

    /// <summary>Deleting a line removes the row.</summary>
    [Fact]
    public async Task DeleteLine_removes_row()
    {
        var (service, dbName) = Fresh();
        await service.CreateLineAsync("AREA-1", "LINE-DEL", "l", null);

        byte[] rv;
        using (var db = UnsTreeTestDb.CreateNamed(dbName))
        {
            rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-DEL").RowVersion;
        }

        var result = await service.DeleteLineAsync("LINE-DEL", rv);

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

        using var verify = UnsTreeTestDb.CreateNamed(dbName);
        verify.UnsLines.Any(l => l.UnsLineId == "LINE-DEL").ShouldBeFalse();
    }

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

        var result = await service.DeleteLineAsync("GHOST", []);

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