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;
///
/// Verifies the Equipment CRUD mutations on : the system-generated
/// EQ- id and fleet-wide MachineCode uniqueness. (v3: equipment no longer binds a driver — the
/// reference-only Tags tab points at raw tags — so the old decision-#122 driver-cluster guard is gone,
/// and its tests with it.)
///
///
/// The EF InMemory provider does not enforce RowVersion concurrency, so the
/// DbUpdateConcurrencyException branches are not exercised here by design.
///
[Trait("Category", "Unit")]
public sealed class UnsTreeServiceEquipmentTests
{
private static (UnsTreeService Service, string DbName) Fresh()
{
var dbName = $"uns-equip-{Guid.NewGuid():N}";
return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
}
/// Seeds an area → line (id LINE-1) under .
private static void SeedLine(string dbName, string cluster)
{
using var db = UnsTreeTestDb.CreateNamed(dbName);
db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = cluster, Name = "a" });
db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" });
db.SaveChanges();
}
private static EquipmentInput Input(string name, string machineCode, string unsLineId) =>
new(name, machineCode, unsLineId,
ZTag: null, SAPID: null, Manufacturer: null, Model: null, SerialNumber: null,
HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: null,
AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: true);
// ----- CreateEquipment -----
/// A new equipment gets a system-generated EQ- id (EQ- + 12 hex chars) and persists.
[Fact]
public async Task CreateEquipment_generates_EQ_id_and_persists()
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"));
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
using var db = UnsTreeTestDb.CreateNamed(dbName);
var eq = db.Equipment.Single(e => e.MachineCode == "machine_001");
eq.EquipmentId.ShouldStartWith("EQ-");
eq.EquipmentId.Length.ShouldBe(15); // "EQ-" + 12 hex chars
eq.Name.ShouldBe("machine-1");
eq.UnsLineId.ShouldBe("LINE-1");
eq.Enabled.ShouldBeTrue();
}
/// Creating equipment with a MachineCode already in the fleet is blocked.
[Fact]
public async Task CreateEquipment_duplicate_machinecode_blocked()
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
await service.CreateEquipmentAsync(Input("machine-1", "machine_dup", "LINE-1"));
var result = await service.CreateEquipmentAsync(Input("machine-2", "machine_dup", "LINE-1"));
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("MachineCode 'machine_dup' already exists in this fleet.");
}
/// Creating equipment with no UNS line returns the pick-a-line error.
[Fact]
public async Task CreateEquipment_missing_line_blocked()
{
var (service, _) = Fresh();
var result = await service.CreateEquipmentAsync(Input("machine-1", "machine_001", ""));
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Pick a UNS line.");
}
/// CreateEquipmentAsync returns the generated EQ- id in CreatedId so callers can navigate to the new page.
[Fact]
public async Task CreateEquipment_returns_generated_id_in_CreatedId()
{
var dbName = $"uns-eq-createdid-{Guid.NewGuid():N}";
UnsTreeTestDb.SeedNamed(dbName);
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
var input = new EquipmentInput("machine-2", "machine_002", "LINE-1",
null, null, null, null, null, null, null, null, null, null, null, true);
var result = await service.CreateEquipmentAsync(input);
result.Ok.ShouldBeTrue();
result.CreatedId.ShouldNotBeNull();
result.CreatedId!.ShouldStartWith("EQ-");
}
// ----- UpdateEquipment -----
/// Updating equipment changes its mutable fields (name, MachineCode, a 40010 field).
[Fact]
public async Task UpdateEquipment_changes_fields()
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"));
string equipmentId;
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
{
var eq = db.Equipment.Single(e => e.MachineCode == "machine_001");
equipmentId = eq.EquipmentId;
rv = eq.RowVersion;
}
var updated = new EquipmentInput("machine-renamed", "machine_renamed", "LINE-1",
ZTag: null, SAPID: null, Manufacturer: "Acme", Model: null, SerialNumber: null,
HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: (short)2021,
AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: false);
var result = await service.UpdateEquipmentAsync(equipmentId, updated, rv);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
using var verify = UnsTreeTestDb.CreateNamed(dbName);
var after = verify.Equipment.Single(e => e.EquipmentId == equipmentId);
after.Name.ShouldBe("machine-renamed");
after.MachineCode.ShouldBe("machine_renamed");
after.Manufacturer.ShouldBe("Acme");
after.YearOfConstruction.ShouldBe((short)2021);
after.Enabled.ShouldBeFalse();
}
/// Updating equipment that no longer exists returns the row-gone error.
[Fact]
public async Task UpdateEquipment_missing_row_returns_error()
{
var (service, _) = Fresh();
var result = await service.UpdateEquipmentAsync("EQ-nope", Input("n", "mc", "LINE-1"), []);
result.Ok.ShouldBeFalse();
result.Error.ShouldBe("Row no longer exists.");
}
/// Updating equipment with a MachineCode that already belongs to another row is blocked.
[Fact]
public async Task UpdateEquipment_duplicate_machinecode_blocked()
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
await service.CreateEquipmentAsync(Input("machine-a", "mc_a", "LINE-1"));
await service.CreateEquipmentAsync(Input("machine-b", "mc_b", "LINE-1"));
string equipmentId;
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
{
var eq = db.Equipment.Single(e => e.MachineCode == "mc_a");
equipmentId = eq.EquipmentId;
rv = eq.RowVersion;
}
// Try to rename mc_a → mc_b (which already exists).
var result = await service.UpdateEquipmentAsync(
equipmentId, Input("machine-a", "mc_b", "LINE-1"), rv);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
result.Error.ShouldBe("MachineCode 'mc_b' already exists in this fleet.");
// The original row must be unchanged.
using var verify = UnsTreeTestDb.CreateNamed(dbName);
verify.Equipment.Single(e => e.EquipmentId == equipmentId).MachineCode.ShouldBe("mc_a");
}
// ----- DeleteEquipment -----
/// Deleting equipment removes the row.
[Fact]
public async Task DeleteEquipment_removes_row()
{
var (service, dbName) = Fresh();
SeedLine(dbName, "MAIN");
await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1"));
string equipmentId;
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
{
var eq = db.Equipment.Single(e => e.MachineCode == "machine_001");
equipmentId = eq.EquipmentId;
rv = eq.RowVersion;
}
var result = await service.DeleteEquipmentAsync(equipmentId, rv);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
using var verify = UnsTreeTestDb.CreateNamed(dbName);
verify.Equipment.Any(e => e.EquipmentId == equipmentId).ShouldBeFalse();
}
/// Deleting equipment that is already gone is a no-op success.
[Fact]
public async Task DeleteEquipment_already_gone_returns_ok()
{
var (service, _) = Fresh();
var result = await service.DeleteEquipmentAsync("EQ-ghost", []);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
}
}