using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Configuration.Validation; namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests; /// /// Verifies materialises a /// from the live config DB whose Tag/VirtualTag rows feed the /// equipment-signal collision rule — the one rule wired into the deploy gate (Task 3). /// [Trait("Category", "Unit")] public sealed class DraftSnapshotFactoryTests : IDisposable { private readonly OtOpcUaConfigDbContext _db; /// Initializes a new instance with an isolated in-memory config DB. public DraftSnapshotFactoryTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"draft-snapshot-{Guid.NewGuid():N}") .Options; _db = new OtOpcUaConfigDbContext(options); } /// Disposes the database context. public void Dispose() => _db.Dispose(); /// Seeds an Equipment plus a raw Tag and a VirtualTag; the snapshot must carry both /// signal collections. v3: Tags are raw-only, so the equipment-signal collision rule now operates /// on VirtualTags — two VirtualTags sharing (EquipmentId, Name) must surface the collision. [Fact] public async Task FromConfigDb_populates_Tags_and_VirtualTags_and_surfaces_collision() { SeedEquipment("eq-1"); _db.Tags.Add(BuildTag(name: "speed")); _db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "a")); _db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "b")); await _db.SaveChangesAsync(); var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db); snapshot.Tags.Count.ShouldBe(1); snapshot.VirtualTags.Count.ShouldBe(2); DraftValidator.Validate(snapshot).ShouldContain(e => e.Code == "UnsEffectiveNameCollision"); } /// Distinct VirtualTag names under the same equipment do not collide, so the snapshot /// validates clean of the collision code. [Fact] public async Task FromConfigDb_no_collision_when_names_differ() { SeedEquipment("eq-1"); _db.Tags.Add(BuildTag(name: "speed")); _db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "temperature")); await _db.SaveChangesAsync(); var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db); snapshot.Tags.Count.ShouldBe(1); snapshot.VirtualTags.Count.ShouldBe(1); DraftValidator.Validate(snapshot).ShouldNotContain(e => e.Code == "UnsEffectiveNameCollision"); } /// Seeds one active and one released reservation for the same equipment context; /// only the active row (ReleasedAt == null) should appear in the snapshot. [Fact] public async Task FromConfigDb_ActiveReservations_excludes_released_rows() { var equipmentUuid = Guid.NewGuid(); _db.ExternalIdReservations.Add(new ExternalIdReservation { ReservationId = Guid.NewGuid(), Kind = ReservationKind.ZTag, Value = "ZT-001", EquipmentUuid = equipmentUuid, ClusterId = "cluster-a", FirstPublishedBy = "test", ReleasedAt = null, // active }); _db.ExternalIdReservations.Add(new ExternalIdReservation { ReservationId = Guid.NewGuid(), Kind = ReservationKind.ZTag, Value = "ZT-002", EquipmentUuid = equipmentUuid, ClusterId = "cluster-a", FirstPublishedBy = "test", ReleasedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), // released ReleasedBy = "test", ReleaseReason = "retired", }); await _db.SaveChangesAsync(); var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db); snapshot.ActiveReservations.Count.ShouldBe(1); snapshot.ActiveReservations[0].Value.ShouldBe("ZT-001"); snapshot.ActiveReservations[0].ReleasedAt.ShouldBeNull(); } private void SeedEquipment(string equipmentId) { var uuid = Guid.NewGuid(); _db.Equipment.Add(new Equipment { EquipmentUuid = uuid, EquipmentId = equipmentId, Name = "eq", UnsLineId = "line-a", MachineCode = "m", }); } // v3: Tags are raw-only (DeviceId FK, no EquipmentId/DriverInstanceId). private static Tag BuildTag(string name) => new() { TagId = $"tag-{name}", DeviceId = "dev-1", Name = name, DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{}", }; private static VirtualTag BuildVirtualTag(string equipmentId, string name, string suffix = "") => new() { VirtualTagId = $"vtag-{name}{suffix}", EquipmentId = equipmentId, Name = name, DataType = "Float", ScriptId = "s-1", }; }