using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests;
///
/// PR 5.4 — Write-by-classification parity. Each driver routes writes by the
/// attribute's : FreeAccess /
/// Operate use plain Write; Tune / Configure /
/// VerifiedWrite use WriteSecured. Both backends must surface the
/// same StatusCode for the same write request — successful for FreeAccess /
/// Operate (assuming the dev Galaxy has at least one writable attribute) and
/// failure for Configure when no auth principal is supplied.
///
[Trait("Category", "ParityE2E")]
[Collection(nameof(ParityCollection))]
public sealed class WriteByClassificationParityTests
{
private readonly ParityHarness _h;
public WriteByClassificationParityTests(ParityHarness h) => _h = h;
[Fact]
public async Task FreeAccess_or_Operate_write_returns_same_StatusCode_on_both_backends()
{
_h.RequireBoth();
var b = new RecordingAddressSpaceBuilder();
await ((ITagDiscovery)_h.LegacyDriver!).DiscoverAsync(b, CancellationToken.None);
var target = b.Variables.FirstOrDefault(v =>
v.AttributeInfo.SecurityClass is SecurityClassification.FreeAccess or SecurityClassification.Operate
&& v.AttributeInfo.DriverDataType is DriverDataType.Float32 or DriverDataType.Float64 or DriverDataType.Int32);
if (target is null) Assert.Skip("no FreeAccess/Operate numeric writable attribute on dev Galaxy");
var request = new[] { new WriteRequest(target.AttributeInfo.FullName, 0.0) };
var results = await _h.RunOnAvailableAsync(
(driver, ct) => ((IWritable)driver).WriteAsync(request, ct),
CancellationToken.None);
var legacyCode = results[ParityHarness.Backend.LegacyHost][0].StatusCode;
var mxgwCode = results[ParityHarness.Backend.MxGateway][0].StatusCode;
AssertStatusClassMatches(legacyCode, mxgwCode, target.AttributeInfo.FullName);
}
[Fact]
public async Task Configure_class_write_routes_through_secured_path_on_both_backends()
{
_h.RequireBoth();
var b = new RecordingAddressSpaceBuilder();
await ((ITagDiscovery)_h.LegacyDriver!).DiscoverAsync(b, CancellationToken.None);
var target = b.Variables.FirstOrDefault(v =>
v.AttributeInfo.SecurityClass is SecurityClassification.Configure or SecurityClassification.Tune);
if (target is null) Assert.Skip("no Configure/Tune attribute on dev Galaxy");
var request = new[] { new WriteRequest(target.AttributeInfo.FullName, 0.0) };
var results = await _h.RunOnAvailableAsync(
(driver, ct) => ((IWritable)driver).WriteAsync(request, ct),
CancellationToken.None);
// Both backends route through the secured-write path. The exact StatusCode
// depends on whether the running test identity has write permission on the
// dev Galaxy — what matters here is that they agree on the status *class*
// (Good vs Bad vs Uncertain), not which exact code they produce.
var legacyCode = results[ParityHarness.Backend.LegacyHost][0].StatusCode;
var mxgwCode = results[ParityHarness.Backend.MxGateway][0].StatusCode;
AssertStatusClassMatches(legacyCode, mxgwCode, target.AttributeInfo.FullName);
}
///
/// Pin the parity invariant that *matters*: both backends classify the same
/// write outcome as Good / Uncertain / Bad. The exact OPC UA code can diverge
/// because legacy MxAccessGalaxyBackend flat-maps every failure to
/// BadInternalError while the new GatewayGalaxyDataWriter uses
/// MxStatusProxy.RawDetectedBy to distinguish gateway-layer faults
/// (BadCommunicationError) from MxAccess HRESULT faults — see
/// docs/v2/Galaxy.ParityMatrix.md "Accepted deltas". Tighter mapping
/// parity isn't worth investing in: legacy retires in PR 7.2.
///
private static void AssertStatusClassMatches(uint legacyCode, uint mxgwCode, string tag)
{
IsBadStatus(legacyCode).ShouldBe(IsBadStatus(mxgwCode),
$"status-class (Bad) parity for '{tag}': legacy=0x{legacyCode:X8}, mxgw=0x{mxgwCode:X8}");
IsGoodStatus(legacyCode).ShouldBe(IsGoodStatus(mxgwCode),
$"status-class (Good) parity for '{tag}': legacy=0x{legacyCode:X8}, mxgw=0x{mxgwCode:X8}");
}
private static bool IsBadStatus(uint code) => (code & 0xC0000000u) == 0x80000000u;
private static bool IsGoodStatus(uint code) => (code & 0xC0000000u) == 0x00000000u;
}