//! The process shell: build the writers, collect `argv`, dispatch, return an
//! exit code. Every command body lives in `cli.zig`, which takes its writers as
//! parameters and is therefore testable without a process.

const std = @import("std");
const cli = @import("cli.zig");
const logging = @import("platform/logging.zig");

/// Routes every `std.log` call through the sink. Before `logging.install`
/// runs (and always under the test runner, which never installs), the sink
/// passes through to the stderr default.
pub const std_options: std.Options = .{ .logFn = logging.logFn };

// `src/tests.zig` imports this file, and this is how `cli.zig`'s tests reach
// the same runner. `src/tests.zig` is the orchestrator's file, not this
// session's.
comptime {
    _ = @import("cli.zig");
}

/// Streaming, never positional, and this is load-bearing rather than a default
/// worth changing back.
///
/// `File.writer` builds a `.positional` writer: it keeps an offset of its own,
/// starting at zero, and pwrites there. `std.log` writes to the same descriptor
/// directly and shares the descriptor's offset. With stderr redirected to a
/// regular file the two disagree about where the end is, so the runner's first
/// flush lands on top of whatever the log already wrote at the start and the
/// operator loses those lines. `writerStreaming` uses the descriptor's own
/// offset, so both writers append.
///
/// A terminal and a pipe are unaffected either way, because neither is seekable
/// and the positional writer falls back to streaming on them. `2>file` is where
/// it shows, and `nxdns run 2>logfile` is an ordinary way to run this program.
/// `>>` needs it too: pwrite ignores `O_APPEND`.
///
/// Takes the `File` rather than naming stdout and stderr inside, so a test can
/// hand it a real file and read back what an operator would have.
fn runnerWriter(file: std.Io.File, io: std.Io, buffer: []u8) std.Io.File.Writer {
    return file.writerStreaming(io, buffer);
}

pub fn main(init: std.process.Init) u8 {
    // Both `File.Writer` values are self-referential and must not move, so they
    // stay in these `var` slots for the whole of `main`.
    var out_buffer: [4096]u8 = undefined;
    var err_buffer: [4096]u8 = undefined;
    var out = runnerWriter(std.Io.File.stdout(), init.io, &out_buffer);
    var err = runnerWriter(std.Io.File.stderr(), init.io, &err_buffer);

    const runner: cli.Runner = .{
        .io = init.io,
        .gpa = init.gpa,
        .out = &out.interface,
        .err = &err.interface,
    };

    var argv: std.ArrayList([]const u8) = .empty;
    defer argv.deinit(init.gpa);

    var args = init.minimal.args.iterate();
    _ = args.skip();
    while (args.next()) |arg| {
        argv.append(init.gpa, arg) catch return cli.exit_runtime;
    }

    const command = cli.parseArgs(argv.items) catch |e| return cli.runUsageError(runner, e);

    return switch (command) {
        .run => |args_| cli.runRun(runner, args_),
        // `true`: the probe leaves the machine, which is right for an operator
        // running `nxdns check` and wrong for a test.
        .check => |args_| cli.runCheck(runner, args_, true),
        .export_ => |args_| cli.runExport(runner, args_),
        .import_ => |args_| cli.runImport(runner, args_),
        .version => cli.runVersion(runner),
        .help => cli.runHelp(runner),
    };
}

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

test "a runner writer appends after what std.log already put in a redirected file" {
    // Reproduced live before this test existed: `nxdns run --config <bad file>
    // 2> file` lost `info(migrations): config.db migrated from schema version 0
    // to 2`, because the runner's error writer pwrote its first flush at offset
    // zero, over the line the log had already written. Down a pipe the same run
    // kept both lines, which is why a terminal never showed this.
    var threaded: std.Io.Threaded = .init(std.testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();
    const gpa = std.testing.allocator;

    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var file = try tmp.dir.createFile(io, "stderr.txt", .{});
    defer file.close(io);

    // What the log sink does: a direct write on the shared descriptor, which
    // moves the descriptor's offset. A positional writer does not see it.
    const log_line = "info(migrations): config.db migrated from schema version 0 to 2\n";
    try file.writeStreamingAll(io, log_line);

    const runner_line = "nxdns run failed: MissingDefaultGroup\n";
    var buffer: [4096]u8 = undefined;
    var w = runnerWriter(file, io, &buffer);
    try w.interface.writeAll(runner_line);
    try w.interface.flush();

    const seen = try tmp.dir.readFileAlloc(io, "stderr.txt", gpa, .limited(4096));
    defer gpa.free(seen);
    try std.testing.expectEqualStrings(log_line ++ runner_line, seen);
}
