using System.Text.Json;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
///
/// PR 7 — array contiguous block addressing. One libplctag tag with elem_count=N
/// pulls N consecutive PCCC words in a single frame. Parser accepts both Rockwell `,N`
/// and libplctag `[N]` suffixes; driver advertises
/// and decodes the buffer element-by-element.
///
[Trait("Category", "Unit")]
public sealed class AbLegacyArrayTests
{
// ---- Parser positives ----
[Theory]
[InlineData("N7:0,10", "N", 7, 0, 10)]
[InlineData("N7:0[10]", "N", 7, 0, 10)]
[InlineData("F8:0,5", "F", 8, 0, 5)]
[InlineData("F8:0[5]", "F", 8, 0, 5)]
[InlineData("B3:0,10", "B", 3, 0, 10)]
[InlineData("L19:0,4", "L", 19, 0, 4)]
[InlineData("N7:0,1", "N", 7, 0, 1)]
[InlineData("N7:0,120", "N", 7, 0, 120)]
public void TryParse_accepts_array_suffix(string input, string letter, int? file, int word, int expectedArrayCount)
{
var a = AbLegacyAddress.TryParse(input);
a.ShouldNotBeNull();
a.FileLetter.ShouldBe(letter);
a.FileNumber.ShouldBe(file);
a.WordNumber.ShouldBe(word);
a.ArrayCount.ShouldBe(expectedArrayCount);
}
[Fact]
public void TryParse_no_suffix_leaves_ArrayCount_null()
{
var a = AbLegacyAddress.TryParse("N7:0");
a.ShouldNotBeNull();
a.ArrayCount.ShouldBeNull();
}
// ---- Parser rejects ----
[Theory]
[InlineData("N7:0,10/3")] // array + bit
[InlineData("N7:0[10]/3")] // array + bit (bracket form)
[InlineData("T4:0,5.ACC")] // array + sub-element
[InlineData("T4:0[5].ACC")] // array + sub-element (bracket form)
[InlineData("N7:0,121")] // over PCCC frame ceiling
[InlineData("N7:0[121]")] // over PCCC frame ceiling (bracket form)
[InlineData("N7:0,0")] // zero-element array
[InlineData("N7:0,-1")] // negative array
[InlineData("N7:0,abc")] // non-numeric array
public void TryParse_rejects_bad_array_combinations(string input)
{
AbLegacyAddress.TryParse(input).ShouldBeNull();
}
// ---- ToLibplctagName canonicalises to bracket form ----
[Theory]
[InlineData("N7:0,10", "N7:0[10]")]
[InlineData("N7:0[10]", "N7:0[10]")]
[InlineData("F8:0,5", "F8:0[5]")]
[InlineData("L19:0,4", "L19:0[4]")]
public void ToLibplctagName_canonicalises_array_suffix_to_brackets(string input, string expectedLibplctag)
{
var a = AbLegacyAddress.TryParse(input);
a.ShouldNotBeNull();
a.ToLibplctagName().ShouldBe(expectedLibplctag);
}
[Fact]
public void ToLibplctagName_array_with_indirect_word()
{
// Indirect word source + array suffix — `N7:[N7:0][10]` reads 10 consecutive words
// starting at the resolved indirect address. (Driver path still rejects indirect
// addresses at create-time but the parser surface should keep them queryable.)
var a = AbLegacyAddress.TryParse("N7:[N7:0][10]");
a.ShouldNotBeNull();
a.ArrayCount.ShouldBe(10);
a.IndirectWordSource.ShouldNotBeNull();
a.ToLibplctagName().ShouldBe("N7:[N7:0][10]");
}
// ---- Options override ----
[Fact]
public void ResolveElementCount_explicit_ArrayLength_overrides_parsed_suffix()
{
// Address says ,10 but ArrayLength override pins to 20 — config wins over the
// address suffix.
var def = new AbLegacyTagDefinition(
"X", "ab://h/1,0", "N7:0,10", AbLegacyDataType.Int, ArrayLength: 20);
var parsed = AbLegacyAddress.TryParse(def.Address)!;
AbLegacyDriver.ResolveElementCount(def, parsed).ShouldBe(20);
}
[Fact]
public void ResolveElementCount_no_override_uses_parsed_suffix()
{
var def = new AbLegacyTagDefinition(
"X", "ab://h/1,0", "N7:0,10", AbLegacyDataType.Int);
var parsed = AbLegacyAddress.TryParse(def.Address)!;
AbLegacyDriver.ResolveElementCount(def, parsed).ShouldBe(10);
}
[Fact]
public void ResolveElementCount_no_override_no_suffix_returns_one()
{
var def = new AbLegacyTagDefinition(
"X", "ab://h/1,0", "N7:0", AbLegacyDataType.Int);
var parsed = AbLegacyAddress.TryParse(def.Address)!;
AbLegacyDriver.ResolveElementCount(def, parsed).ShouldBe(1);
}
[Fact]
public void ResolveElementCount_oversized_override_throws()
{
var def = new AbLegacyTagDefinition(
"X", "ab://h/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 121);
var parsed = AbLegacyAddress.TryParse(def.Address)!;
Should.Throw(() => AbLegacyDriver.ResolveElementCount(def, parsed));
}
[Fact]
public void ResolveElementCount_zero_override_throws()
{
var def = new AbLegacyTagDefinition(
"X", "ab://h/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 0);
var parsed = AbLegacyAddress.TryParse(def.Address)!;
Should.Throw(() => AbLegacyDriver.ResolveElementCount(def, parsed));
}
// ---- DTO JSON round-trip ----
[Fact]
public void DriverConfigJson_round_trips_ArrayLength()
{
const string json = """
{
"Devices": [
{ "HostAddress": "ab://10.0.0.5/1,0", "PlcFamily": "Slc500" }
],
"Tags": [
{
"Name": "Block",
"DeviceHostAddress": "ab://10.0.0.5/1,0",
"Address": "N7:0,10",
"DataType": "Int",
"ArrayLength": 20
}
]
}
""";
var driver = AbLegacyDriverFactoryExtensions.CreateInstance("drv-1", json);
// Reflect into the live driver to confirm the tag definition carries the override.
// Cleaner than serialising back out — the DTO → record flow is what we actually care about.
// Using ReadAsync with a fake factory would also work but adds a mile of plumbing.
// Here we just confirm the round-trip via the public ResolveElementCount surface.
var parsedAddr = AbLegacyAddress.TryParse("N7:0,10")!;
var def = new AbLegacyTagDefinition(
"Block", "ab://10.0.0.5/1,0", "N7:0,10", AbLegacyDataType.Int, ArrayLength: 20);
AbLegacyDriver.ResolveElementCount(def, parsedAddr).ShouldBe(20);
driver.ShouldNotBeNull();
}
// ---- Runtime ElementCount plumbing ----
[Fact]
public async Task EnsureTagRuntime_threads_ElementCount_through_to_factory()
{
var factory = new FakeAbLegacyTagFactory();
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition(
"Block", "ab://10.0.0.5/1,0", "N7:0,10", AbLegacyDataType.Int)],
}, "drv-1", factory);
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
factory.Customise = p => new FakeAbLegacyTag(p)
{
ArrayValues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
};
await drv.ReadAsync(["Block"], TestContext.Current.CancellationToken);
// Tag name canonicalises to the libplctag bracket form; that's what shows up as the key.
factory.Tags.ShouldContainKey("N7:0[10]");
factory.Tags["N7:0[10]"].CreationParams.ElementCount.ShouldBe(10);
}
[Fact]
public async Task EnsureTagRuntime_uses_ArrayLength_override_when_set()
{
var factory = new FakeAbLegacyTagFactory();
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition(
"Block", "ab://10.0.0.5/1,0", "N7:0,10", AbLegacyDataType.Int, ArrayLength: 20)],
}, "drv-1", factory);
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
factory.Customise = p => new FakeAbLegacyTag(p)
{
ArrayValues = Enumerable.Range(0, 20).Cast