using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Maintenance;
///
/// Pure verdict table of (ScadaBridge#107 item 1) for the
/// branches the SQL Server tests do not reach: a link carried by a tombstone, the partition
/// anchor itself, and malformed input.
///
public sealed class AuditChainHeadComparerTests
{
private static readonly byte[] H1 = Enumerable.Repeat((byte)1, 32).ToArray();
private static readonly byte[] H2 = Enumerable.Repeat((byte)2, 32).ToArray();
private static AuditChainHeadRecord Exported(long seq, byte[] hash) => new(
1, "2026-05", seq, AuditChainHeadComparer.ToHex(hash), null, null, seq, 0, DateTime.UtcNow, DateTime.UtcNow, "central-a");
private static AuditChainHeadSnapshot Head(long seq, byte[] hash, long? anchor = null, byte[]? anchorHash = null) =>
new(new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc), seq, hash, anchor, anchorHash, seq, 0, DateTime.UtcNow);
private static Func> Link(byte[]? row, byte[]? tombstone)
=> _ => Task.FromResult(new AuditChainHeadComparer.StoredLink(row, tombstone));
private static Func> NoLookup
=> _ => throw new InvalidOperationException("no link lookup expected");
/// The exported link was later purged per channel: the tombstone carries the hash.
/// A task that represents the asynchronous operation.
[Fact]
public async Task GrownChain_LinkOnATombstone_IsExtended_OrMismatch()
{
Assert.Equal(AuditChainHeadStatus.Extended,
(await AuditChainHeadComparer.CompareAsync(Exported(3, H1), Head(9, H2), Link(null, H1))).Status);
Assert.Equal(AuditChainHeadStatus.Mismatch,
(await AuditChainHeadComparer.CompareAsync(Exported(3, H1), Head(9, H2), Link(null, H2))).Status);
}
/// The exported head is the partition-purge anchor: compared with the anchor's hash, no lookup.
/// A task that represents the asynchronous operation.
[Fact]
public async Task GrownChain_ExportedHeadIsTheAnchor_ComparesTheAnchorHash()
{
Assert.Equal(AuditChainHeadStatus.Extended,
(await AuditChainHeadComparer.CompareAsync(Exported(4, H1), Head(9, H2, 4, H1), NoLookup)).Status);
Assert.Equal(AuditChainHeadStatus.Mismatch,
(await AuditChainHeadComparer.CompareAsync(Exported(4, H1), Head(9, H2, 4, H2), NoLookup)).Status);
Assert.Equal(AuditChainHeadStatus.Purged,
(await AuditChainHeadComparer.CompareAsync(Exported(2, H1), Head(9, H2, 4, H2), NoLookup)).Status);
}
/// A record whose hash is not 32 bytes of hex can never match.
/// The exported hash text.
/// A task that represents the asynchronous operation.
[Theory]
[InlineData("abc")]
[InlineData("zz0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")]
public async Task MalformedExportedHash_IsMismatch(string hash)
{
var record = Exported(3, H1) with { LastHash = hash };
Assert.Equal(AuditChainHeadStatus.Mismatch,
(await AuditChainHeadComparer.CompareAsync(record, Head(3, H1), NoLookup)).Status);
}
/// Only the tamper verdicts count as failures.
/// The verdict.
/// Whether it is a failure.
[Theory]
[InlineData(AuditChainHeadStatus.Matches, false)]
[InlineData(AuditChainHeadStatus.Extended, false)]
[InlineData(AuditChainHeadStatus.Purged, false)]
[InlineData(AuditChainHeadStatus.Mismatch, true)]
[InlineData(AuditChainHeadStatus.Regressed, true)]
[InlineData(AuditChainHeadStatus.MissingHead, true)]
[InlineData(AuditChainHeadStatus.MissingLink, true)]
[InlineData(AuditChainHeadStatus.CountMismatch, true)]
public void IsFailure_IsTrueOnlyForTamperVerdicts(AuditChainHeadStatus status, bool failure)
{
Assert.Equal(failure, new AuditChainHeadCheck("2026-05", 1, DateTime.UtcNow, null, status, 1, "").IsFailure);
}
}