//! Global pause of filtering (PLAN §13.1). One 64-bit flag the query path
//! reads and the API writes. Pure: no allocation, no `std.Io`, no clock — the
//! caller supplies the time, because the handler already has it.
//!
//! Pausing suspends FILTERING only: local records, forward zones, the cache,
//! the upstream and the query log all keep running (milestone-7 ruling 18).
//!
//! The state is in memory and is deliberately not persisted: a restart resumes
//! filtering, which is the safe default for a household.

const std = @import("std");

pub const Pause = struct {
    /// 0 = filtering active. -1 = paused until someone resumes. Any other value
    /// is the unix second filtering resumes at, so the expiry is a comparison
    /// on the query path rather than a timer task.
    until: std.atomic.Value(i64) = .init(0),

    /// `.monotonic` throughout: the flag guards no other data, so nothing has
    /// to be ordered against it.
    pub fn isPaused(self: *const Pause, now_s: i64) bool {
        const until = self.until.load(.monotonic);
        if (until == 0) return false;
        if (until < 0) return true;
        return now_s < until;
    }

    /// `null` pauses until `unpause`. A pause already in force is replaced, so
    /// the last call wins whether it lengthens or shortens the pause.
    pub fn pauseFor(self: *Pause, now_s: i64, duration_s: ?u32) void {
        const value: i64 = if (duration_s) |seconds| now_s +| @as(i64, seconds) else -1;
        self.until.store(value, .monotonic);
    }

    pub fn unpause(self: *Pause) void {
        self.until.store(0, .monotonic);
    }
};

const testing = std.testing;

test "a fresh pause is not paused" {
    const p: Pause = .{};
    try testing.expect(!p.isPaused(0));
    try testing.expect(!p.isPaused(1_700_000_000));
}

test "an indefinite pause holds at every time" {
    var p: Pause = .{};
    p.pauseFor(1_700_000_000, null);
    try testing.expect(p.isPaused(1_700_000_000));
    try testing.expect(p.isPaused(1_700_000_000 + 86_400 * 365));
    try testing.expectEqual(@as(i64, -1), p.until.load(.monotonic));
}

test "a timed pause expires at its own second" {
    var p: Pause = .{};
    p.pauseFor(1_000, 60);
    try testing.expect(p.isPaused(1_000));
    try testing.expect(p.isPaused(1_059));
    // The stored second is when filtering is back on, so it is not paused.
    try testing.expect(!p.isPaused(1_060));
    try testing.expect(!p.isPaused(1_061));
}

test "unpause resumes both kinds of pause" {
    var p: Pause = .{};
    p.pauseFor(1_000, null);
    p.unpause();
    try testing.expect(!p.isPaused(1_000));

    p.pauseFor(1_000, 60);
    p.unpause();
    try testing.expect(!p.isPaused(1_000));
}

test "pauseFor overwrites a pause already in force" {
    var p: Pause = .{};
    p.pauseFor(1_000, 3_600);
    p.pauseFor(1_000, 10);
    try testing.expect(!p.isPaused(1_010));

    // And in the other direction: indefinite replaces a timed pause.
    p.pauseFor(1_000, 10);
    p.pauseFor(1_000, null);
    try testing.expect(p.isPaused(1_010));
}

test "a duration that would overflow saturates instead of wrapping" {
    var p: Pause = .{};
    p.pauseFor(std.math.maxInt(i64), std.math.maxInt(u32));
    try testing.expect(p.isPaused(std.math.maxInt(i64) - 1));
}
