using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests; [Trait("Category", "Unit")] public sealed class SnapshotFormatterTests { private static readonly DateTime FixedTime = new(2026, 4, 21, 12, 34, 56, 789, DateTimeKind.Utc); [Fact] public void Format_includes_tag_value_status_and_both_timestamps() { var snap = new DataValueSnapshot(42, 0u, FixedTime, FixedTime); var output = SnapshotFormatter.Format("N7:0", snap); output.ShouldContain("Tag: N7:0"); output.ShouldContain("Value: 42"); output.ShouldContain("Status: 0x00000000 (Good)"); output.ShouldContain("Source Time: 2026-04-21T12:34:56.789Z"); output.ShouldContain("Server Time: 2026-04-21T12:34:56.789Z"); } [Theory] [InlineData(0x00000000u, "Good")] [InlineData(0x80000000u, "Bad")] [InlineData(0x80050000u, "BadCommunicationError")] [InlineData(0x80060000u, "BadTimeout")] [InlineData(0x80340000u, "BadNodeIdUnknown")] [InlineData(0x40000000u, "Uncertain")] public void FormatStatus_names_well_known_status_codes(uint status, string expectedName) { SnapshotFormatter.FormatStatus(status).ShouldContain(expectedName); } [Fact] public void FormatStatus_unknown_codes_fall_back_to_hex_only() { // 0xDEADBEEF isn't in the shortlist — just render the hex form, no name. SnapshotFormatter.FormatStatus(0xDEADBEEFu).ShouldBe("0xDEADBEEF"); } [Fact] public void FormatValue_renders_null_as_placeholder() { var snap = new DataValueSnapshot(null, 0x80050000u, null, FixedTime); var output = SnapshotFormatter.Format("Orphan", snap); output.ShouldContain("Value: "); output.ShouldContain("Source Time: -"); // null timestamp → dash } [Fact] public void FormatValue_formats_booleans_lowercase() { var snap = new DataValueSnapshot(true, 0u, FixedTime, FixedTime); SnapshotFormatter.Format("Coil", snap).ShouldContain("Value: true"); } [Fact] public void FormatValue_formats_floats_invariant_culture() { // Guards against non-invariant decimal separators (e.g. comma on PL locales) // that would break cross-platform log diffs. var snap = new DataValueSnapshot(3.14f, 0u, FixedTime, FixedTime); SnapshotFormatter.Format("F8:0", snap).ShouldContain("3.14"); } [Fact] public void FormatValue_quotes_strings() { var snap = new DataValueSnapshot("hello", 0u, FixedTime, FixedTime); SnapshotFormatter.Format("Msg", snap).ShouldContain("\"hello\""); } [Fact] public void FormatWrite_shows_status_with_tag_name() { var result = new WriteResult(0u); SnapshotFormatter.FormatWrite("Scratch", result) .ShouldBe("Write Scratch: 0x00000000 (Good)"); } [Fact] public void FormatTable_aligns_columns_and_includes_header_separator() { var names = new[] { "A", "LongerTag" }; var snaps = new[] { new DataValueSnapshot(1, 0u, FixedTime, FixedTime), new DataValueSnapshot(2, 0u, FixedTime, FixedTime), }; var table = SnapshotFormatter.FormatTable(names, snaps); table.ShouldContain("TAG"); table.ShouldContain("VALUE"); table.ShouldContain("STATUS"); table.ShouldContain("SOURCE TIME"); table.ShouldContain("---"); // separator row table.ShouldContain("LongerTag"); table.ShouldContain("0x00000000"); } [Fact] public void FormatTable_rejects_mismatched_lengths() { Should.Throw(() => SnapshotFormatter.FormatTable( new[] { "A", "B" }, new[] { new DataValueSnapshot(1, 0u, FixedTime, FixedTime) })); } [Fact] public void FormatTimestamp_normalises_local_kind_to_utc() { // Unspecified / Local times must land on UTC in the output — otherwise a CI box in // UTC+X would emit diffs against dev-laptop runs. var local = new DateTime(2026, 4, 21, 8, 0, 0, DateTimeKind.Local); var formatted = SnapshotFormatter.FormatTimestamp(local); formatted.ShouldEndWith("Z"); } }