using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests;
///
/// Gitea #674.1 — pins the QuestDB TTL behaviour the historian's retention relies on: a table TTL is judged against
/// the wall clock (min(max timestamp, now)), so one far-future row — a 2066 typo that reached the backend
/// without going through the node manager's HistoryUpdateMaxFutureSkew bound — cannot age out every older
/// partition, including the Part 11 modification trail (the record of who changed history).
///
///
///
/// Positive control. A partition genuinely older than the TTL (40 days, against a 30-day TTL) is kept until
/// the far-future commit and evicted BY it — proving TTL housekeeping ran on exactly the commit that could have
/// dropped the in-TTL partitions. Without it the "nothing evicted" assertion would also pass against a table
/// whose TTL never ran.
///
///
/// ⚠️ Observed on QuestDB 10.0.1 (2026-10-11): the in-TTL partitions also survived with
/// cairo.ttl.use.wall.clock=false (verified effective via SHOW PARAMETERS), so this test does not
/// isolate that flag — it pins the outcome. Keep the flag at its default (true) regardless; the server-side
/// future-skew bound is the primary guard.
///
///
[Collection(QuestDbCollection.Name)]
public sealed class TtlWallClockTests(QuestDbFixture fx)
{
private static CancellationToken Ct => TestContext.Current.CancellationToken;
private static readonly HistoryUpdateContext Who = new("ttl-test", null);
[Fact]
public async Task A_far_future_row_evicts_only_partitions_older_than_the_ttl_by_the_wall_clock()
{
fx.SkipIfUnavailable();
var o = fx.Options(fx.UniquePrefix(), retentionDays: 30);
var c = new QuestDbConnection(o);
var wal = new QuestDbWalVisibility(c, o.VisibilityTimeout);
var tags = new QuestDbTagCatalog(c, new QuestDbSchemaBootstrapper(c, o), wal);
var updater = new QuestDbHistorianDataUpdater(c, tags, wal);
// Whole seconds (µs-aligned), one row per DAY partition. With the newest row 10 days old, the 40-day partition is
// still inside the TTL measured from the table's max timestamp, so this commit evicts nothing.
var now = DateTime.UtcNow;
var today = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, DateTimeKind.Utc);
DateTime[] older = [today.AddDays(-40), today.AddDays(-20), today.AddDays(-10)];
(await updater.UpdateDataAsync("ttl.tag", HistoryUpdateMode.Insert, older.Select((t, i) => V(t, i)).ToList(), Who, Ct))
.ShouldAllBe(code => code == HistorianStatusCodes.GoodEntryInserted);
(await DaysAsync(c, c.SampleTable)).ShouldBe(older.Select(t => t.Date));
(await DaysAsync(c, c.SampleModifiedTable)).ShouldBe(older.Select(t => t.Date));
// The far-future row, then one more ordinary commit (TTL housekeeping runs on commits).
(await updater.UpdateDataAsync("ttl.tag", HistoryUpdateMode.Insert,
[V(new DateTime(2066, 1, 1, 0, 0, 0, DateTimeKind.Utc), 9)], Who, Ct)).ShouldBe([HistorianStatusCodes.GoodEntryInserted]);
(await updater.UpdateDataAsync("ttl.tag", HistoryUpdateMode.Insert, [V(today, 10)], Who, Ct))
.ShouldBe([HistorianStatusCodes.GoodEntryInserted]);
(await wal.WaitAsync(c.SampleTable, Ct)).ShouldBeTrue();
(await wal.WaitAsync(c.SampleModifiedTable, Ct)).ShouldBeTrue();
// Only the genuinely expired 40-day partition is gone; the 20- and 10-day partitions survive in both tables.
DateTime[] expected = [today.AddDays(-20).Date, today.AddDays(-10).Date, today.Date, new(2066, 1, 1)];
(await DaysAsync(c, c.SampleTable)).ShouldBe(expected);
(await DaysAsync(c, c.SampleModifiedTable)).ShouldBe(expected);
}
private static HistorizationValue V(DateTime t, double v) => new(t, HistorianValue.FromDouble(v), HistorianStatusCodes.Good);
/// The distinct UTC days holding rows, ascending.
private static async Task> DaysAsync(QuestDbConnection c, string table)
{
await using var conn = await c.OpenAsync(Ct);
await using var cmd = c.CreateCommand(conn, $"SELECT ts FROM {table} ORDER BY ts");
await using var r = await cmd.ExecuteReaderAsync(Ct);
var days = new SortedSet();
while (await r.ReadAsync(Ct)) days.Add(QuestDbConnection.ReadUtc(r, 0).Date);
return days.ToList();
}
}