//! `POST /api/certs/reload` — reload the DoH/DoT certificates from disk
//! (milestone-10 ruling 8).
//!
//! Always answers 200: the per-endpoint outcome IS the payload. A failed
//! reload is an outcome, not a server error — the store publishes nothing on
//! failure (PLAN:297), so the old certificate keeps serving and nothing about
//! the web server's own state is exceptional. A disabled endpoint has no
//! store and reports `{enabled: false, reloaded: false, error: null}`.

const std = @import("std");

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

const Request = http_util.Request;
const HandlerError = http_util.HandlerError;

pub const Outcome = struct {
    enabled: bool,
    reloaded: bool,
    /// `cert_store.humanMessage` text; null on success and while disabled.
    @"error": ?[]const u8,
};

pub const View = struct {
    doh: Outcome,
    dot: Outcome,
};

/// Reloads every enabled endpoint's store. A null store is a disabled
/// endpoint: the composition root only constructs one for an enabled
/// endpoint (milestone-10 ruling 11).
pub fn applyReload(state: *server.WebState, io: std.Io) View {
    return .{
        .doh = outcome(state.doh_certs, io),
        .dot = outcome(state.dot_certs, io),
    };
}

fn outcome(store: ?*cert_store.CertStore, io: std.Io) Outcome {
    const live = store orelse return .{ .enabled = false, .reloaded = false, .@"error" = null };
    live.reload(io) catch |err| return .{
        .enabled = true,
        .reloaded = false,
        .@"error" = cert_store.humanMessage(err),
    };
    return .{ .enabled = true, .reloaded = true, .@"error" = null };
}

pub fn post(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
    return http_util.respondJson(request, .ok, applyReload(state, io), &.{});
}

// ---------------------------------------------------------------------------
// tests
// ---------------------------------------------------------------------------

const fixtures = @import("test_fixtures");
const testing = std.testing;

test "both endpoints disabled report exactly the disabled outcome" {
    var state: server.WebState = .{ .gpa = testing.allocator };
    const view = applyReload(&state, undefined);
    for ([_]Outcome{ view.doh, view.dot }) |per_endpoint| {
        try testing.expect(!per_endpoint.enabled);
        try testing.expect(!per_endpoint.reloaded);
        try testing.expectEqual(@as(?[]const u8, null), per_endpoint.@"error");
    }
}

/// Fixture PEMs in a tmp directory, addressed the way `app.zig` hands config
/// paths to the store. Must not move after `init`: the path slices point into
/// the buffers below.
const TestEnv = struct {
    threaded: std.Io.Threaded,
    tmp: testing.TmpDir,
    cert_path_buf: [128]u8,
    key_path_buf: [128]u8,
    cert_path: []const u8,
    key_path: []const u8,

    fn init(env: *TestEnv) !void {
        env.threaded = .init(testing.allocator, .{});
        errdefer env.threaded.deinit();
        env.tmp = testing.tmpDir(.{});
        errdefer env.tmp.cleanup();

        const env_io = env.threaded.io();
        try env.tmp.dir.writeFile(env_io, .{ .sub_path = "cert.pem", .data = fixtures.cert_pem });
        try env.tmp.dir.writeFile(env_io, .{ .sub_path = "key.pem", .data = fixtures.key_pem });
        env.cert_path = try std.fmt.bufPrint(&env.cert_path_buf, ".zig-cache/tmp/{s}/cert.pem", .{env.tmp.sub_path});
        env.key_path = try std.fmt.bufPrint(&env.key_path_buf, ".zig-cache/tmp/{s}/key.pem", .{env.tmp.sub_path});
    }

    fn deinit(env: *TestEnv) void {
        env.tmp.cleanup();
        env.threaded.deinit();
    }

    fn io(env: *TestEnv) std.Io {
        return env.threaded.io();
    }
};

test "a wired store reloads; a broken one reports the message in the same payload" {
    var env: TestEnv = undefined;
    try env.init();
    defer env.deinit();
    const io = env.io();

    var store = try cert_store.CertStore.init(testing.allocator, io, env.cert_path, env.key_path, null);
    defer store.deinit(io);

    var state: server.WebState = .{ .gpa = testing.allocator, .doh_certs = &store };

    const succeeded = applyReload(&state, io);
    try testing.expect(succeeded.doh.enabled);
    try testing.expect(succeeded.doh.reloaded);
    try testing.expectEqual(@as(?[]const u8, null), succeeded.doh.@"error");
    try testing.expect(!succeeded.dot.enabled);
    try testing.expectEqual(@as(u64, 1), store.snapshotStats().reloads);

    // The certificate file vanishes: the outcome names the failure, the store
    // keeps its old generation, and the dot half is untouched.
    try env.tmp.dir.deleteFile(io, "cert.pem");
    const failed = applyReload(&state, io);
    try testing.expect(failed.doh.enabled);
    try testing.expect(!failed.doh.reloaded);
    try testing.expectEqualStrings(
        cert_store.humanMessage(error.CertUnreadable),
        failed.doh.@"error".?,
    );
    try testing.expect(!failed.dot.enabled);
    try testing.expectEqual(@as(u64, 1), store.snapshotStats().reload_failures);
}
