using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
[Trait("Category", "Unit")]
public sealed class FocasPmcBitRmwTests
{
///
/// Fake client simulating PMC byte storage as a single 1024-byte buffer. Post-F4-c
/// (issue #270) the FOCAS driver routes PMC writes through the typed
/// +
/// entry points — the bit path performs RMW via ReadPmcRangeAsync +
/// WritePmcRangeAsync, so this fake overrides those to drive a shared
/// buffer the tests can assert against.
/// is the unit-test surface; we mirror writes to
/// too so any helper that reads from there sees the same source of truth.
///
private sealed class PmcRmwFake : FakeFocasClient
{
public byte[] PmcBytes { get; } = new byte[1024];
public override Task<(byte[]? buffer, uint status)> ReadPmcRangeAsync(
string letter, int pathId, int startByte, int byteCount, CancellationToken ct)
{
var buf = new byte[byteCount];
Array.Copy(PmcBytes, startByte, buf, 0, byteCount);
return Task.FromResult<(byte[]?, uint)>((buf, FocasStatusMapper.Good));
}
public override Task WritePmcRangeAsync(
string letter, int pathId, int startByte, byte[] bytes, CancellationToken ct)
{
Array.Copy(bytes, 0, PmcBytes, startByte, bytes.Length);
return Task.FromResult(FocasStatusMapper.Good);
}
}
private static (FocasDriver drv, PmcRmwFake fake) NewDriver(params FocasTagDefinition[] tags)
{
var fake = new PmcRmwFake();
var factory = new FakeFocasClientFactory { Customise = () => fake };
// PMC bit RMW exercises the write path; opt every supplied tag into Writable + flip the
// driver-level Writes.Enabled gate so the tests still drive the wire path after F4-a's
// safer-by-default flip (issue #268).
var writableTags = tags
.Select(t => t with { Writable = true })
.ToArray();
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Tags = writableTags,
Probe = new FocasProbeOptions { Enabled = false },
Writes = new FocasWritesOptions { Enabled = true, AllowPmc = true },
}, "drv-1", factory);
return (drv, fake);
}
[Fact]
public async Task Bit_set_surfaces_as_Good_status_and_flips_bit()
{
var (drv, fake) = NewDriver(
new FocasTagDefinition("Run", "focas://10.0.0.5:8193", "R100.3", FocasDataType.Bit));
await drv.InitializeAsync("{}", CancellationToken.None);
fake.PmcBytes[100] = 0b0000_0001;
var results = await drv.WriteAsync([new WriteRequest("Run", true)], CancellationToken.None);
results.Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
fake.PmcBytes[100].ShouldBe((byte)0b0000_1001);
}
[Fact]
public async Task Bit_clear_preserves_other_bits()
{
var (drv, fake) = NewDriver(
new FocasTagDefinition("Flag", "focas://10.0.0.5:8193", "R100.3", FocasDataType.Bit));
await drv.InitializeAsync("{}", CancellationToken.None);
fake.PmcBytes[100] = 0xFF;
await drv.WriteAsync([new WriteRequest("Flag", false)], CancellationToken.None);
fake.PmcBytes[100].ShouldBe((byte)0b1111_0111);
}
[Fact]
public async Task Subsequent_bit_sets_in_same_byte_compose_correctly()
{
var tags = Enumerable.Range(0, 8)
.Select(b => new FocasTagDefinition($"Bit{b}", "focas://10.0.0.5:8193", $"R100.{b}", FocasDataType.Bit))
.ToArray();
var (drv, fake) = NewDriver(tags);
await drv.InitializeAsync("{}", CancellationToken.None);
fake.PmcBytes[100] = 0;
for (var b = 0; b < 8; b++)
await drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None);
fake.PmcBytes[100].ShouldBe((byte)0xFF);
}
[Fact]
public async Task Bit_write_to_different_bytes_does_not_contend()
{
var tags = Enumerable.Range(0, 4)
.Select(i => new FocasTagDefinition($"Bit{i}", "focas://10.0.0.5:8193", $"R{50 + i}.0", FocasDataType.Bit))
.ToArray();
var (drv, fake) = NewDriver(tags);
await drv.InitializeAsync("{}", CancellationToken.None);
await Task.WhenAll(Enumerable.Range(0, 4).Select(i =>
drv.WriteAsync([new WriteRequest($"Bit{i}", true)], CancellationToken.None)));
for (var i = 0; i < 4; i++)
fake.PmcBytes[50 + i].ShouldBe((byte)0x01);
}
}