using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
///
/// #650 — DATA_SET / TABLE observations are served as a String carrying compact, deterministic JSON.
/// Pins the parse (entries, cells, removed, resetTriggered) and the index's merge semantics
/// (/current replaces, /sample merges, UNAVAILABLE drops the accumulated set).
///
public sealed class MTConnectDataSetTests
{
private const uint Good = 0x00000000u;
private const uint BadNoCommunication = 0x80310000u;
private static string Doc(string events, string attrs = "") =>
$"""
{events}
""";
private static string DataSet(string body, string attrs = "") =>
$"""{body}""";
private static string Table(string body, string attrs = "") =>
$"""{body}""";
private static MTConnectObservationIndex NewIndex() =>
new([new MTConnectTagDefinition("ds", DriverDataType.String), new MTConnectTagDefinition("tb", DriverDataType.String)]);
private static void Apply(MTConnectObservationIndex idx, string events, bool baseline = false) =>
idx.Apply(MTConnectStreamsParser.Parse(Doc(events)), baseline);
[Fact]
public void Parser_captures_entries_cells_removed_and_reset()
{
var result = MTConnectStreamsParser.Parse(Doc(
DataSet("""1""", " resetTriggered=\"MANUAL\"") +
Table("""| 1.5 | 2 | """)));
var ds = result.Observations[0];
ds.ResetTriggered.ShouldBeTrue();
ds.Entries!.Count.ShouldBe(2);
ds.Entries[0].ShouldBe(new MTConnectDataSetEntry("a", "1", false));
ds.Entries[1].Removed.ShouldBeTrue();
var tb = result.Observations[1];
tb.Entries!.ShouldHaveSingleItem().Cells!.Select(c => (c.Key, c.Value))
.ShouldBe([("X", "1.5"), ("Y", "2")]);
}
[Fact]
public void Data_set_is_served_as_sorted_compact_json_with_numbers_and_strings()
{
var idx = NewIndex();
Apply(idx, DataSet("""71.25A & "B"007"""), baseline: true);
var snap = idx.Get("ds");
snap.StatusCode.ShouldBe(Good);
snap.Value.ShouldBe("""{"name":"A & \"B\"","offsetX":1.25,"part":"007","toolNumber":7}""");
}
[Fact]
public void Table_is_served_as_nested_json()
{
var idx = NewIndex();
Apply(idx, Table("""3 | 2 | 1.0 | """), baseline: true);
idx.Get("tb").Value.ShouldBe("""{"G54":{"X":1.0,"Y":2},"G55":{"X":3}}""");
}
[Fact]
public void Sample_delta_merges_and_removed_entry_disappears()
{
var idx = NewIndex();
Apply(idx, DataSet("""12"""), baseline: true);
Apply(idx, DataSet("""3x"""));
idx.Get("ds").Value.ShouldBe("""{"a":1,"b":3,"c":"x"}""");
Apply(idx, DataSet(""""""));
idx.Get("ds").Value.ShouldBe("""{"b":3,"c":"x"}""");
}
[Fact]
public void Table_row_delta_replaces_the_row_and_removed_row_disappears()
{
var idx = NewIndex();
Apply(idx, Table("""1 | 2 | 3 | """), baseline: true);
Apply(idx, Table("""9 | """));
idx.Get("tb").Value.ShouldBe("""{"G54":{"X":9}}""");
}
[Fact]
public void Current_baseline_replaces_rather_than_merges()
{
var idx = NewIndex();
Apply(idx, DataSet("""12"""), baseline: true);
Apply(idx, DataSet("""3"""), baseline: true);
idx.Get("ds").Value.ShouldBe("""{"c":3}""");
}
[Fact]
public void ResetTriggered_replaces_the_accumulated_set_on_a_sample()
{
var idx = NewIndex();
Apply(idx, DataSet("""1"""), baseline: true);
Apply(idx, DataSet("""9""", " resetTriggered=\"MANUAL\""));
idx.Get("ds").Value.ShouldBe("""{"z":9}""");
}
[Fact]
public void ResetTriggered_with_no_entries_is_an_empty_set_not_an_absence()
{
var idx = NewIndex();
Apply(idx, DataSet("""1"""), baseline: true);
Apply(idx, DataSet("", " resetTriggered=\"MANUAL\""));
var snap = idx.Get("ds");
snap.StatusCode.ShouldBe(Good);
snap.Value.ShouldBe("{}");
}
[Fact]
public void Unavailable_is_BadNoCommunication_and_drops_the_accumulated_set()
{
var idx = NewIndex();
Apply(idx, DataSet("""1"""), baseline: true);
Apply(idx, DataSet("UNAVAILABLE"));
var snap = idx.Get("ds");
snap.StatusCode.ShouldBe(BadNoCommunication);
snap.Value.ShouldBeNull();
// A delta after the outage must not resurrect the pre-outage entry.
Apply(idx, DataSet("""2"""));
idx.Get("ds").Value.ShouldBe("""{"b":2}""");
}
[Fact]
public void Several_observations_in_one_document_fold_in_order()
{
var idx = NewIndex();
Apply(idx, DataSet("""1""") + DataSet("""2"""), baseline: true);
idx.Get("ds").Value.ShouldBe("""{"a":1,"b":2}""");
}
[Fact]
public void Clear_forgets_the_accumulated_set()
{
var idx = NewIndex();
Apply(idx, DataSet("""1"""), baseline: true);
idx.Clear();
Apply(idx, DataSet("""2"""));
idx.Get("ds").Value.ShouldBe("""{"b":2}""");
}
[Fact]
public void Structured_content_that_is_not_entries_stays_BadNotSupported()
{
var idx = NewIndex();
Apply(idx, DataSet("1"), baseline: true);
idx.Get("ds").StatusCode.ShouldBe(0x803D0000u);
}
[Fact]
public void ResetTriggered_on_an_ordinary_sample_keeps_its_numeric_value()
{
// Regression: resetTriggered is also a standard attribute on accumulating SAMPLEs / counters. It must
// never turn one into an empty set.
var idx = new MTConnectObservationIndex(
[new MTConnectTagDefinition("pc", DriverDataType.Int64), new MTConnectTagDefinition("amt", DriverDataType.Float64)]);
idx.Apply(MTConnectStreamsParser.Parse(Doc(
"""5""" +
"""2.5""")));
idx.Get("pc").StatusCode.ShouldBe(Good);
idx.Get("pc").Value.ShouldBe(5L);
idx.Get("amt").Value.ShouldBe(2.5d);
}
[Fact]
public void An_empty_current_data_set_is_Good_empty_json_like_a_reset_one()
{
var idx = NewIndex();
Apply(idx, DataSet("""1"""), baseline: true);
Apply(idx, DataSet(""), baseline: true);
var snap = idx.Get("ds");
snap.StatusCode.ShouldBe(Good);
snap.Value.ShouldBe("{}");
}
[Fact]
public void An_all_removed_set_is_the_same_empty_json()
{
var idx = NewIndex();
Apply(idx, DataSet("""1"""), baseline: true);
Apply(idx, DataSet(""""""));
idx.Get("ds").Value.ShouldBe("{}");
}
}