//! Network-dependent test for `dot_client.zig`.
//!
//! Separate file because it needs `@import("build_options")`, which only exists
//! when build.zig drives the compilation. It is compiled by every
//! `zig build test` run, so it cannot rot, and skips at run time without
//! `-Dlive`. (`-Dintegration` stays hermetic; `-Dlive` is the gate for tests
//! that leave the machine.)

const std = @import("std");
const build_options = @import("build_options");
const tls = std.crypto.tls;
const Certificate = std.crypto.Certificate;

const dot_client = @import("dot_client.zig");
const transport = @import("transport.zig");
const packet = @import("../dns/packet.zig");

/// Neither `connect` nor a TLS stream read accepts a timeout in 0.16.0, so the
/// whole exchange runs as one task raced against a sleep and the loser is
/// canceled.
const budget: std.Io.Clock.Duration = .{ .raw = .fromSeconds(10), .clock = .awake };

/// An A query for example.com: id 0x1234, RD set, one question.
const query_bytes =
    "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
    "\x07example\x03com\x00\x00\x01\x00\x01";

/// This machine's IPv6 egress is dead and upstream name resolution is out of
/// scope, so the documented anycast IPv4 literal is used.
const upstream_url = "tls://1.1.1.1:853";

/// The name Cloudflare publishes for this endpoint. Without it the handshake is
/// `error.CertificateHostMismatch`: `std.crypto.Certificate` matches dNSName
/// SANs only, so the IP SAN on the leaf certificate is never consulted.
const upstream_tls_name = "one.one.one.one";

const Outcome = union(enum) {
    exchange: anyerror!usize,
    expiry: std.Io.Cancelable!void,
};

const Params = struct {
    gpa: std.mem.Allocator,
    bundle: *Certificate.Bundle,
    bundle_lock: *std.Io.RwLock,
    buffers: dot_client.DotClient.Buffers,
    response_buf: []u8,
};

fn runExchange(io: std.Io, params: Params) anyerror!usize {
    const endpoint: transport.Endpoint = try .parse(upstream_url);
    var client: dot_client.DotClient = .init(
        endpoint,
        upstream_tls_name,
        params.gpa,
        params.bundle,
        params.bundle_lock,
        null,
        params.buffers,
    );
    // The client owns the session it dials and keeps it open past the exchange,
    // so the task that built the client is what has to close it. Without this
    // the socket and its TLS state outlive the test.
    defer client.close(io);
    var selected: ?[]const u8 = null;
    const reply = try client.client().exchange(io, query_bytes, params.response_buf, &selected);
    std.debug.assert(std.mem.eql(u8, selected.?, endpoint.url));
    return reply.len;
}

fn expire(io: std.Io, duration: std.Io.Clock.Duration) std.Io.Cancelable!void {
    return duration.sleep(io);
}

test "live DoT exchange against 1.1.1.1" {
    if (!build_options.live) return error.SkipZigTest;

    const gpa = std.testing.allocator;

    var threaded: std.Io.Threaded = .init(gpa, .{});
    defer threaded.deinit();
    const io = threaded.io();

    var bundle: Certificate.Bundle = .empty;
    defer bundle.deinit(gpa);
    var bundle_lock: std.Io.RwLock = .init;

    const chunk = tls.Client.min_buffer_len;
    const scratch = try gpa.alloc(u8, 4 * chunk);
    defer gpa.free(scratch);

    var response_buf: [transport.max_message_len]u8 = undefined;

    var outcomes: [2]Outcome = undefined;
    var race: std.Io.Select(Outcome) = .init(io, &outcomes);
    defer race.cancelDiscard();

    try race.concurrent(.exchange, runExchange, .{ io, Params{
        .gpa = gpa,
        .bundle = &bundle,
        .bundle_lock = &bundle_lock,
        .buffers = .{
            .tls_read = scratch[0..chunk],
            .tls_write = scratch[chunk .. 2 * chunk],
            .stream_read = scratch[2 * chunk .. 3 * chunk],
            .stream_write = scratch[3 * chunk ..],
        },
        .response_buf = &response_buf,
    } });
    try race.concurrent(.expiry, expire, .{ io, budget });

    const len = switch (try race.await()) {
        .exchange => |result| result catch |err| {
            std.debug.print("DoT exchange with {s} failed: {s}\n", .{
                upstream_url,
                @errorName(err),
            });
            return err;
        },
        .expiry => |result| {
            try result;
            return error.DotExchangeTimedOut;
        },
    };

    // `exchange` already ran `transport.validateResponse`, so the id, question
    // and QR bit are known good. What is left to check is that the upstream
    // actually answered the question.
    const reply = try packet.parse(response_buf[0..len]);
    try std.testing.expect(reply.header.ancount >= 1);
}
