//! `GET /api/version` — what this binary is and how long it has been running.
//!
//! Unauthenticated (ruling 18), like the other monitoring endpoints. The three
//! strings are build options, so nothing here reads the running configuration.

const std = @import("std");

const http_util = @import("../http_util.zig");
const server = @import("../server.zig");
const version = @import("../../version.zig");

pub const Body = struct {
    version: []const u8,
    git_commit: []const u8,
    zig_version: []const u8,
    /// Seconds since the process started. Zero until `started_unix` is wired,
    /// and never negative: a clock stepped backwards must not report a
    /// process that started in the future.
    uptime_seconds: u64,
};

pub fn handle(
    state: *server.WebState,
    io: std.Io,
    request: *http_util.Request,
) http_util.HandlerError!void {
    const now = std.Io.Clock.real.now(io).toSeconds();
    return http_util.respondJson(request, .ok, body(state.version, state.started_unix, now), &.{});
}

pub fn body(version_string: []const u8, started_unix: i64, now_unix: i64) Body {
    return .{
        .version = if (version_string.len == 0) version.string else version_string,
        .git_commit = version.git_commit,
        .zig_version = version.zig_version_string,
        .uptime_seconds = uptime(started_unix, now_unix),
    };
}

fn uptime(started_unix: i64, now_unix: i64) u64 {
    if (started_unix <= 0 or now_unix <= started_unix) return 0;
    return @intCast(now_unix - started_unix);
}

const testing = std.testing;

test "the body carries the build strings and the elapsed time" {
    const out = body("", 1_000, 1_060);
    try testing.expectEqualStrings(version.string, out.version);
    try testing.expectEqualStrings(version.git_commit, out.git_commit);
    try testing.expectEqualStrings(version.zig_version_string, out.zig_version);
    try testing.expectEqual(@as(u64, 60), out.uptime_seconds);
}

test "the state's version string wins over the compiled-in one" {
    try testing.expectEqualStrings("9.9.9", body("9.9.9", 0, 0).version);
}

test "an unset start time and a clock that stepped back both read as zero uptime" {
    try testing.expectEqual(@as(u64, 0), body("", 0, 5_000).uptime_seconds);
    try testing.expectEqual(@as(u64, 0), body("", 5_000, 4_000).uptime_seconds);
}

test "the body serializes with snake_case field names" {
    var buffer: [256]u8 = undefined;
    var writer: std.Io.Writer = .fixed(&buffer);
    try std.json.Stringify.value(body("1.2.3", 10, 20), .{}, &writer);
    const text = writer.buffered();
    try testing.expect(std.mem.containsAtLeast(u8, text, 1, "\"git_commit\":"));
    try testing.expect(std.mem.containsAtLeast(u8, text, 1, "\"zig_version\":"));
    try testing.expect(std.mem.containsAtLeast(u8, text, 1, "\"uptime_seconds\":10"));
}
