using Shouldly;
using TwinCAT.TypeSystem;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
///
/// Pins — how a bit-indexed BOOL tag (.ADSSTATE_RUN.0) is served by a
/// native notification. ADS has no symbol for WORD.N: live on the TwinCAT 2.11 Elma cleaner Z33856
/// (2026-10-07) registering the notification on .ADSSTATE_RUN.0 failed with
/// DeviceSymbolNotFound — and that registration error tore down the WHOLE subscribe call. The
/// notification now registers the PARENT word as raw bytes (its resolved width) and extracts the bit.
///
[Trait("Category", "Unit")]
public sealed class TwinCATBitAccessTests
{
/// The parent path is the bit path without its trailing .N; a non-bit path is unchanged.
[Fact]
public void ParentPath_strips_the_trailing_bit_index()
{
TwinCATBitAccess.ParentPath(".ADSSTATE_RUN.0").ShouldBe(".ADSSTATE_RUN");
TwinCATBitAccess.ParentPath("MAIN.Status.Flags.15").ShouldBe("MAIN.Status.Flags");
TwinCATBitAccess.ParentPath("MAIN.Flags").ShouldBe("MAIN.Flags");
}
/// Bits are numbered little-endian across the parent's bytes (bit 9 = byte 1, bit 1).
[Fact]
public void ReadBit_numbers_bits_little_endian_across_the_parent_bytes()
{
byte[] word = [0b0000_0101, 0b0000_0010]; // .ADSSTATE_RUN = 5 → bits 0 and 2; plus bit 9
TwinCATBitAccess.ReadBit(word, 0).ShouldBeTrue();
TwinCATBitAccess.ReadBit(word, 1).ShouldBeFalse();
TwinCATBitAccess.ReadBit(word, 2).ShouldBeTrue();
TwinCATBitAccess.ReadBit(word, 9).ShouldBeTrue();
TwinCATBitAccess.ReadBit(word, 15).ShouldBeFalse();
}
/// A bit past the parent's width cannot be read — refused instead of publishing FALSE at Good.
[Fact]
public void CheckParent_refuses_a_bit_outside_the_parent_width()
{
var word = TwinCATPlcSymbolShape.Classify("UINT16", DataTypeCategory.Primitive, 2);
TwinCATBitAccess.CheckParent(15, word).ShouldBeNull();
TwinCATBitAccess.CheckParent(16, word).ShouldNotBeNull();
TwinCATBitAccess.CheckParent(-1, word).ShouldNotBeNull();
}
/// Only an integer word has addressable bits — a string, a float or a container is refused.
[Fact]
public void CheckParent_refuses_a_parent_that_is_not_an_integer_word()
{
TwinCATBitAccess.CheckParent(0, TwinCATPlcSymbolShape.Classify("STRING(80)", DataTypeCategory.String, 81)).ShouldNotBeNull();
TwinCATBitAccess.CheckParent(0, TwinCATPlcSymbolShape.Classify("REAL", DataTypeCategory.Primitive, 4)).ShouldNotBeNull();
TwinCATBitAccess.CheckParent(0, TwinCATPlcSymbolShape.Classify("ST_X", DataTypeCategory.Struct, 8)).ShouldNotBeNull();
TwinCATBitAccess.CheckParent(31, TwinCATPlcSymbolShape.Classify("DWORD", DataTypeCategory.Primitive, 4)).ShouldBeNull();
}
// ---- #739 H18: the bit WRITE is a read-modify-write at the parent's own width ----
/// A bit write flips exactly one bit of the parent's raw bytes at the parent's width.
[Fact]
public void ApplyBitWrite_flips_one_bit_at_the_parent_width()
{
var word = TwinCATPlcSymbolShape.Classify("WORD", DataTypeCategory.Primitive, 2);
TwinCATBitAccess.ApplyBitWrite(word, [0x01, 0x00], 9, true, out var set).ShouldBe(TwinCATStatusMapper.Good);
set.ShouldBe(new byte[] { 0x01, 0x02 });
TwinCATBitAccess.ApplyBitWrite(word, [0xFF, 0xFF], 3, false, out var cleared).ShouldBe(TwinCATStatusMapper.Good);
cleared.ShouldBe(new byte[] { 0xF7, 0xFF });
}
///
/// A signed parent with its sign bit set is plain bytes — the old UDInt round-trip read it back as
/// a value ≥ 2³¹ and a checked conversion turned ANY bit write into BadOutOfRange.
///
[Fact]
public void ApplyBitWrite_on_a_negative_signed_parent_preserves_the_sign_bit()
{
var intWord = TwinCATPlcSymbolShape.Classify("INT", DataTypeCategory.Primitive, 2);
TwinCATBitAccess.ApplyBitWrite(intWord, [0x00, 0x80], 0, true, out var updated).ShouldBe(TwinCATStatusMapper.Good);
updated.ShouldBe(new byte[] { 0x01, 0x80 });
}
/// A bit beyond the parent is BadOutOfRange; a non-integer parent BadTypeMismatch; nothing is produced.
[Fact]
public void ApplyBitWrite_refuses_a_bit_the_parent_does_not_have()
{
var word = TwinCATPlcSymbolShape.Classify("UINT", DataTypeCategory.Primitive, 2);
TwinCATBitAccess.ApplyBitWrite(word, [0, 0], 16, true, out var none).ShouldBe(TwinCATStatusMapper.BadOutOfRange);
none.ShouldBeEmpty();
var real = TwinCATPlcSymbolShape.Classify("REAL", DataTypeCategory.Primitive, 4);
TwinCATBitAccess.ApplyBitWrite(real, [0, 0, 0, 0], 1, true, out _).ShouldBe(TwinCATStatusMapper.BadTypeMismatch);
}
/// A parent read shorter than the parent's width is never written back (it would zero bytes).
[Fact]
public void ApplyBitWrite_refuses_a_short_parent_buffer()
{
var dword = TwinCATPlcSymbolShape.Classify("DWORD", DataTypeCategory.Primitive, 4);
TwinCATBitAccess.ApplyBitWrite(dword, [0xFF, 0xFF], 1, true, out var none)
.ShouldBe(TwinCATStatusMapper.BadCommunicationError);
none.ShouldBeEmpty();
}
}