//! The serving handler: raw bytes from a listener in, bytes to send back out.
//! It owns no socket and no allocator — the listener supplies the buffers, the
//! upstream supplies the answer, and every collaborator is an optional pointer
//! the composition root fills in.
//!
//! `handle` returns no error union. Every failure is either a DNS response the
//! client can act on or a counted drop, because a listener has nothing useful
//! to do with an error value: it cannot retry and it must not die. That is the
//! "every failure mode is visible" rule applied to the hot path — the counters
//! are the failure surface. The same rule is why every mutex here is taken with
//! `lockUncancelable`: `handle` has no error union to carry `error.Canceled`
//! out of, and each critical section is a bounded, I/O-free table access.
//!
//! The pipeline is PLAN §4: rate limit → parse → group → local records →
//! forward zones → filtering → safe search → cache → upstream → CNAME
//! uncloaking → cache store → query log. It logs nothing per query: the query
//! log is a database row, and `std.log` is reserved for failures nobody else
//! records.

const std = @import("std");

const address = @import("../platform/address.zig");
const clients = @import("clients.zig");
const dns_cache = @import("../cache/dns_cache.zig");
const edns = @import("../dns/edns.zig");
const forward_client = @import("../local/forward_client.zig");
const forward_zones = @import("../local/forward_zones.zig");
const header = @import("../dns/header.zig");
const local_tables_mod = @import("local_tables.zig");
const logger_controller = @import("../storage/logger_controller.zig");
const logger_mod = @import("../storage/logger.zig");
const manager = @import("../filter/manager.zig");
const matcher = @import("../filter/matcher.zig");
const model = @import("../config/model.zig");
const name = @import("../dns/name.zig");
const packet = @import("../dns/packet.zig");
const provenance = @import("../storage/provenance.zig");
const pause = @import("pause.zig");
const query_sink = @import("query_sink.zig");
const question = @import("../dns/question.zig");
const rate_limiter = @import("rate_limiter.zig");
const record = @import("../dns/record.zig");
const records = @import("../local/records.zig");
const response = @import("../filter/response.zig");
const retention_mod = @import("../storage/retention.zig");
const safe_url = @import("../safe_url.zig");
const safesearch = @import("../filter/safesearch.zig");
const transport = @import("../upstream/transport.zig");
const upstream_owner = @import("../upstream/owner.zig");
const types = @import("../dns/types.zig");
const validate = @import("../config/validate.zig");

/// Which listener a query arrived on. Only the UDP size limit depends on it.
pub const Transport = enum { udp, tcp };

/// RFC 1035 §4.2.1: 512 bytes is what a client accepts over UDP without EDNS,
/// and RFC 6891 §6.2.3 says a smaller advertised size must not be honoured
/// below that floor.
pub const udp_limit_min: u16 = types.max_udp_payload;

/// The ceiling nxdns advertises and accepts. Above this, fragmentation and
/// reflection amplification cost more than a TCP retry.
pub const udp_limit_max: u16 = 4096;

/// CNAME links followed when uncloaking (PLAN §6.3). A chain longer than this
/// is either a loop or an attempt to outrun the walk.
pub const max_cname_depth = 8;

/// `udp://` or `tcp://`, an IPv6 literal in brackets, and a port. A forward
/// zone's resolver text shares the entry's upstream buffer with the pool's
/// redacted identity, so the wider of the two is what has to fit.
const max_resolver_text = "tcp://[".len + logger_mod.max_client_len + "]:65535".len;

comptime {
    std.debug.assert(max_resolver_text <= logger_mod.max_upstream_len);
}

/// A synthesized reply is a header, at most one question and at most one OPT
/// record: 12 + (255 + 4) + 11 = 282 bytes worst case. Every buffer this file
/// builds into is at least `udp_limit_min`, so the builder cannot run out of
/// room and its `Writer.Error` is unreachable.
const max_synthetic_len = types.header_len + types.max_name_len + 4 + 11;

comptime {
    std.debug.assert(max_synthetic_len <= udp_limit_min);
}

/// What a handler with no `local_tables` reads: no local record and no forward
/// zone. Static, so the null case costs a pointer rather than a branch in every
/// stage that consults them.
const empty_records: records.Records = .empty;
const empty_zones: forward_zones.Zones = .empty;

/// The per-query configuration, as one value. Every query copies it once at
/// query start and reads nothing else afterwards, so a `setPolicy` landing
/// mid-query can never make one query answer with the new blocking TTL and the
/// old ECS mode: a query runs entirely under the policy it started with.
pub const Policy = struct {
    blocking: response.Options,
    ecs_mode: model.EcsMode = .strip,
    forward_read_timeout: std.Io.Clock.Duration,
    /// `cfg.cache.negative_ttl_max`. `DnsCache` keeps no copy of its config and
    /// `dns_cache.classify` needs the value, so the policy carries it. Zero
    /// disables negative caching, which is what a cache-less handler wants.
    negative_ttl_max: u32 = 0,
};

pub const Handler = struct {
    /// The published upstream generation. A query pins one for the length of
    /// its exchange and copies out everything it keeps, so a `replace` can land
    /// mid-query without freeing anything the query still reads.
    upstream: *upstream_owner.Owner,
    /// Read through `policySnapshot`, published through `setPolicy`. The
    /// filter manager's discipline (manager.zig:438) — a shared lock for the
    /// copy, an exclusive one for the swap. A handler that is not serving yet
    /// may be built field by field; once queries run, `setPolicy` is the only
    /// writer.
    policy: Policy,
    policy_lock: std.Io.RwLock = .init,
    manager: ?*manager.Manager = null,
    /// The published local records and forward zones (milestone-8 ruling 12).
    /// Null means neither table exists, which is what a handler built for one
    /// upstream test wants; the API rebuilds and swaps them while queries run.
    local_tables: ?*local_tables_mod.LocalTables = null,
    cache: ?*dns_cache.DnsCache = null,
    cache_mutex: std.Io.Mutex = .init,
    limiter: ?*rate_limiter.RateLimiter = null,
    limiter_mutex: std.Io.Mutex = .init,
    sink: ?*query_sink.QuerySink = null,
    pause: ?*pause.Pause = null,
    tracker: ?*clients.Tracker = null,
    stats: Stats = .{},

    pub const Stats = struct {
        /// Queries answered by the upstream pool.
        queries: std.atomic.Value(u64) = .init(0),
        dropped_malformed: std.atomic.Value(u64) = .init(0),
        formerr: std.atomic.Value(u64) = .init(0),
        notimp: std.atomic.Value(u64) = .init(0),
        /// Queries asking for an EDNS version this server does not implement,
        /// answered with BADVERS (RFC 6891 §6.1.3).
        badvers: std.atomic.Value(u64) = .init(0),
        servfail: std.atomic.Value(u64) = .init(0),
        truncated: std.atomic.Value(u64) = .init(0),
        refused: std.atomic.Value(u64) = .init(0),
        blocked: std.atomic.Value(u64) = .init(0),
        uncloak_blocked: std.atomic.Value(u64) = .init(0),
        local_answers: std.atomic.Value(u64) = .init(0),
        forward_zone_answers: std.atomic.Value(u64) = .init(0),
        /// The three forward-client counters, folded in after every exchange.
        /// `ForwardClient` is built per query and dropped with the query, so
        /// these are where its numbers survive. Its `queries` counter is not
        /// mirrored: `forward_zone_answers` already counts the exchanges.
        forward_udp_truncated: std.atomic.Value(u64) = .init(0),
        forward_foreign_datagrams: std.atomic.Value(u64) = .init(0),
        forward_failures: std.atomic.Value(u64) = .init(0),
        cache_hits: std.atomic.Value(u64) = .init(0),
        paused_queries: std.atomic.Value(u64) = .init(0),
        /// Queries answered before the first snapshot existed, so no group and
        /// no filtering applied (ruling 4).
        unfiltered_queries: std.atomic.Value(u64) = .init(0),
        safesearch_rewrites: std.atomic.Value(u64) = .init(0),
        ecs_strip_failed: std.atomic.Value(u64) = .init(0),
        /// Mirrors the tracker's own `dropped_full`, refreshed on every query
        /// from what `Tracker.track` returns: a counter that only the tracker
        /// holds would not appear beside the rest of these.
        tracker_full: std.atomic.Value(u64) = .init(0),
    };

    pub const Outcome = union(enum) {
        /// A prefix of `response_buf`.
        reply: []u8,
        /// No response is possible or appropriate.
        drop,
    };

    /// The copy every query takes at its start. The shared lock is held for
    /// the copy alone: nothing downstream may hold it across an exchange.
    pub fn policySnapshot(self: *Handler, io: std.Io) Policy {
        self.policy_lock.lockSharedUncancelable(io);
        defer self.policy_lock.unlockShared(io);
        return self.policy;
    }

    /// Publish. Queries already running finish under the policy they copied;
    /// every query that starts after this returns reads `p`.
    pub fn setPolicy(h: *Handler, io: std.Io, p: Policy) void {
        h.policy_lock.lockUncancelable(io);
        defer h.policy_lock.unlock(io);
        h.policy = p;
    }

    /// Publishes a new cache and returns the old one for the caller to retire.
    /// Under `cache_mutex`, which is also where every query loads the pointer,
    /// so no query can be inside the cache this displaces.
    ///
    /// The entries of the old cache are lost. A resize rebuilds the table
    /// anyway, and copying entries across would have to re-derive every TTL
    /// against the new clock for no benefit a household would notice.
    pub fn replaceCache(h: *Handler, io: std.Io, next: ?*dns_cache.DnsCache) ?*dns_cache.DnsCache {
        h.cache_mutex.lockUncancelable(io);
        defer h.cache_mutex.unlock(io);
        const old = h.cache;
        h.cache = next;
        return old;
    }

    /// The rate limiter's equivalent. The in-flight windows are lost, so a
    /// client mid-burst gets a fresh allowance — accepted: the alternative is
    /// carrying counters across a table whose capacity just changed.
    pub fn replaceRateLimiter(
        h: *Handler,
        io: std.Io,
        next: ?*rate_limiter.RateLimiter,
    ) ?*rate_limiter.RateLimiter {
        h.limiter_mutex.lockUncancelable(io);
        defer h.limiter_mutex.unlock(io);
        const old = h.limiter;
        h.limiter = next;
        return old;
    }

    /// `response_buf.len` must be >= 512 and <= 65535.
    pub fn handle(
        self: *Handler,
        io: std.Io,
        which: Transport,
        from: address.NetAddress,
        query: []const u8,
        response_buf: []u8,
        scratch: *Scratch,
    ) Outcome {
        std.debug.assert(response_buf.len >= udp_limit_min);
        std.debug.assert(response_buf.len <= transport.max_message_len);

        // Once, before anything decides anything: every stage below, and every
        // refusal path that logs, reads this copy and never the live policy.
        const policy = self.policySnapshot(io);

        // Fewer than 12 bytes: there is no ID and no question, so no reply can
        // be addressed to this query (PLAN §6.1).
        const hdr = header.parse(query) catch {
            bump(&self.stats.dropped_malformed);
            return .drop;
        };

        // A response on a listener port is either a misdirected reply or a
        // reflection attempt. Answering it would make this server the amplifier.
        if (hdr.flags.qr) {
            bump(&self.stats.dropped_malformed);
            return .drop;
        }

        // Ruling 8: the limit is charged before the parse, so a flood of
        // malformed queries costs one header read each. The REFUSED reply needs
        // the ID, which is why the header is read first. Localhost is not
        // exempt.
        {
            // Same discipline as the cache: the pointer is loaded inside the
            // mutex `replaceRateLimiter` swaps it under.
            self.limiter_mutex.lockUncancelable(io);
            const allowed = if (self.limiter) |limiter|
                limiter.check(std.Io.Clock.awake.now(io), from.key())
            else
                true;
            self.limiter_mutex.unlock(io);
            if (!allowed) {
                return synthesize(hdr, null, null, .refused, &self.stats.refused, response_buf);
            }
        }

        const p = packet.parse(query) catch |err| switch (err) {
            error.Truncated => {
                bump(&self.stats.dropped_malformed);
                return .drop;
            },
            // The header is intact but a section is not. The question is what
            // failed to parse, so nothing is echoed and no OPT is trusted.
            else => return synthesize(hdr, null, null, .form_err, &self.stats.formerr, response_buf),
        };

        // RFC 6891 §6.1.1 puts the OPT record in the additional section, and §7
        // answers a violation with FORMERR. `packet.parse` already rejects a
        // second OPT wherever it sits; the case it accepts and this walk does
        // not is a lone OPT in the answer or authority section, which
        // `findOptRecord` cannot see.
        if (containsOpt(packet.answers(p)) or containsOpt(packet.authorities(p))) {
            const echo = if (hdr.qdcount == 1) packet.firstQuestion(p) else null;
            const out = synthesize(hdr, echo, null, .form_err, &self.stats.formerr, response_buf);
            if (echo) |q| self.logRefusal(io, policy, which, from, query, p, hdr, q, null, response_buf, scratch, out.reply);
            return out;
        }

        // RFC 6891 §6.1.1 also gives the OPT record a root owner name. An OPT
        // that is present but unusable must not be forwarded as if the query
        // carried none: the reply's OPT echo would then disappear with nothing
        // to say it ever existed.
        const opt: ?edns.OptRecord = if (packet.findOptRecord(p)) |rec|
            edns.parseOpt(query, rec) catch {
                const echo = if (hdr.qdcount == 1) packet.firstQuestion(p) else null;
                const out = synthesize(hdr, echo, null, .form_err, &self.stats.formerr, response_buf);
                if (echo) |q| self.logRefusal(io, policy, which, from, query, p, hdr, q, null, response_buf, scratch, out.reply);
                return out;
            }
        else
            null;

        // RFC 6891 §6.1.3: a query naming an EDNS version this server does not
        // implement is answered with BADVERS, and the reply's OPT reports the
        // highest version the server does implement. The check precedes the
        // opcode check because the version governs the whole EDNS exchange,
        // whatever the query asks for.
        if (opt) |o| {
            if (o.version != 0) {
                bump(&self.stats.badvers);
                const echo = if (hdr.qdcount == 1) packet.firstQuestion(p) else null;
                const bytes = build(hdr, echo, o, edns.badvers, false, response_buf);
                if (echo) |q| self.logRefusal(io, policy, which, from, query, p, hdr, q, o, response_buf, scratch, bytes);
                return .{ .reply = bytes };
            }
        }

        if (hdr.flags.opcode != .query) {
            const out = synthesize(hdr, null, opt, .not_imp, &self.stats.notimp, response_buf);
            // The reply echoes no question — an opcode this server does not
            // implement has no defined question semantics — but the query named
            // one, so the row can say which name was refused.
            if (hdr.qdcount == 1) if (packet.firstQuestion(p)) |q| {
                self.logRefusal(io, policy, which, from, query, p, hdr, q, opt, response_buf, scratch, out.reply);
            };
            return out;
        }

        // RFC 9619: exactly one question, in both directions. Any other count
        // has no valid interpretation, so there is no question to echo either.
        if (hdr.qdcount != 1) {
            return synthesize(hdr, null, opt, .form_err, &self.stats.formerr, response_buf);
        }

        // `parse` already walked the question section, so a packet with
        // qdcount 1 always has a first question. The fallback keeps a
        // hand-assembled `Packet` from turning into a crash.
        const q = packet.firstQuestion(p) orelse
            return synthesize(hdr, null, opt, .form_err, &self.stats.formerr, response_buf);

        // One clock read serves the log's elapsed time and every cache
        // timestamp, so nothing inside one query disagrees about when it ran.
        const started = std.Io.Clock.real.now(io);

        if (self.tracker) |tracker| {
            self.stats.tracker_full.store(tracker.track(io, from), .monotonic);
        }

        // Ruling 4: no snapshot means no group and no filtering, and the query
        // is answered anyway — DNS availability beats filtering for a
        // household.
        const acquired = if (self.manager) |m| m.acquire(io) else null;
        defer if (acquired) |snapshot| snapshot.release(io);
        const snapshot: ?*const matcher.Snapshot = if (acquired) |a| a.snapshot else null;
        if (snapshot == null) bump(&self.stats.unfiltered_queries);

        // Ruling 12: the local tables are published the same way the snapshot
        // is, so one query reads one generation of both and the API can swap
        // either while queries run.
        const local = if (self.local_tables) |tables| tables.acquire(io) else null;
        defer if (local) |held| held.release(io);

        const group = if (snapshot) |s| s.groupForClient(from) else 0;
        var ctx: Context = .{
            .handler = self,
            .io = io,
            .policy = policy,
            .which = which,
            .from = from,
            .query = query,
            .response_buf = response_buf,
            .scratch = scratch,
            .hdr = hdr,
            .p = p,
            .q = q,
            .opt = opt,
            .do_bit = if (opt) |o| o.do_bit else false,
            .started = started,
            .now_s = started.toSeconds(),
            .snapshot = snapshot,
            .records = if (local) |held| held.records else &empty_records,
            .zones = if (local) |held| held.zones else &empty_zones,
            .group = group,
            .domain = matcher.normalize(q.name, &scratch.normalize),
            .qclass = @intFromEnum(q.qclass),
            // Borrowed from the snapshot this query holds a handle on, so both
            // outlive the log call.
            .group_id = if (snapshot) |s| s.groups[group].id else null,
            .group_name = if (snapshot) |s| s.groups[group].name else "",
        };
        return ctx.run();
    }

    /// S3.3: a request refused on protocol grounds — BADVERS, NOTIMP, an OPT
    /// record that will not parse or sits in the wrong section — still names a
    /// question, so it is a logged row and not only a counter. `bytes` is the
    /// reply already encoded, because the logged RCODE is read back off the
    /// packet the client receives.
    ///
    /// Refusals that precede a parsed question — the rate limit, an unreadable
    /// packet, a QDCOUNT other than one — stay counters: there is no name to
    /// put in a row.
    fn logRefusal(
        self: *Handler,
        io: std.Io,
        policy: Policy,
        which: Transport,
        from: address.NetAddress,
        query: []const u8,
        p: packet.Packet,
        hdr: header.Header,
        q: question.Question,
        opt: ?edns.OptRecord,
        response_buf: []u8,
        scratch: *Scratch,
        bytes: []const u8,
    ) void {
        if (self.sink == null) return;

        const started = std.Io.Clock.real.now(io);
        var ctx: Context = .{
            .handler = self,
            .io = io,
            .policy = policy,
            .which = which,
            .from = from,
            .query = query,
            .response_buf = response_buf,
            .scratch = scratch,
            .hdr = hdr,
            .p = p,
            .q = q,
            .opt = opt,
            .do_bit = if (opt) |o| o.do_bit else false,
            .started = started,
            .now_s = started.toSeconds(),
            .domain = matcher.normalize(q.name, &scratch.normalize),
            .qclass = @intFromEnum(q.qclass),
            .policy_reason = .protocol_error,
            .route_kind = .rejected,
        };
        ctx.log(bytes);
    }
};

/// Per-query working memory, owned by the listener's slot so that no query path
/// allocates. Every buffer is used by one query at a time.
pub const Scratch = struct {
    normalize: [types.max_name_len]u8,
    key: [dns_cache.max_key_len]u8,
    /// The rewritten outgoing query.
    build: [512]u8,
    /// Forward-zone client frame buffer.
    frame: [forward_client.min_frame_buf]u8,
    /// The safe-search reply, and before that the ECS-stripped form of a
    /// safe-search query — the two never overlap in time, because the outgoing
    /// query is spent once the upstream has answered.
    synth: [4096]u8,
    /// Uncloaking normalizes each CNAME target while `normalize` still holds
    /// the queried name the log entry needs.
    uncloak: [types.max_name_len]u8,
};

/// What the pipeline is allowed to do on the upstream path. Filtering is off
/// while paused (ruling 18) and for a non-IN class (ruling 9), which also skips
/// the cache.
const Mode = struct { filter: bool, cache: bool };

/// The fields every stage of one query shares. It exists so that the stages can
/// be separate functions without threading a dozen parameters through each.
const Context = struct {
    handler: *Handler,
    io: std.Io,
    /// This query's copy, taken once in `handle`. No stage reads
    /// `handler.policy`; that is what makes one query internally consistent.
    policy: Policy,
    which: Transport,
    from: address.NetAddress,
    query: []const u8,
    response_buf: []u8,
    scratch: *Scratch,
    hdr: header.Header,
    p: packet.Packet,
    q: question.Question,
    opt: ?edns.OptRecord,
    do_bit: bool,
    started: std.Io.Timestamp,
    now_s: i64,
    snapshot: ?*const matcher.Snapshot = null,
    /// Borrowed from the `LocalTables` handle this query holds, so both tables
    /// belong to one generation and neither can be freed mid-query.
    records: *const records.Records = &empty_records,
    zones: *const forward_zones.Zones = &empty_zones,
    group: u32 = 0,
    /// The queried name, normalized into `scratch.normalize`.
    domain: []const u8,

    // --- provenance ---------------------------------------------------------
    //
    // Every stage that decides something about the query records it here, and
    // `log` reads the finished set once. The text fields are Context-local
    // buffers rather than slices because the matcher's `Decision.matched` and
    // the uncloak walk both borrow from scratch buffers that the next chain
    // step overwrites (matcher.zig `Candidates`, `scratch.uncloak`).
    qclass: u16,
    /// Null with no snapshot: no group applied, so none is named.
    group_id: ?i64 = null,
    /// Snapshot-owned, so it lives as long as this query's handle.
    group_name: []const u8 = "",
    policy_action: provenance.PolicyAction = .not_evaluated,
    policy_reason: provenance.PolicyReason = .no_match,
    matched_buf: [logger_mod.max_matched_len]u8 = undefined,
    matched_len: u16 = 0,
    source_id: ?i64 = null,
    source_name: []const u8 = "",
    cname_buf: [logger_mod.max_domain_len]u8 = undefined,
    cname_len: u8 = 0,
    safe_search_buf: [logger_mod.max_domain_len]u8 = undefined,
    safe_search_len: u8 = 0,
    route_kind: provenance.RouteKind = .upstream,
    /// Borrowed from the forward-zone table this query holds a handle on.
    forward_zone: []const u8 = "",
    /// The resolver identity of the exchange this query attempted, already
    /// redacted. Empty on every route that attempted none, which reaches the
    /// row as NULL: a cache hit, a local record and a blocked answer name no
    /// upstream (ruling 20's replacement — the `"pool"` and `"local"` markers
    /// are gone).
    upstream_buf: [logger_mod.max_upstream_len]u8 = undefined,
    upstream_len: u16 = 0,

    fn notePolicy(
        ctx: *Context,
        action: provenance.PolicyAction,
        reason: provenance.PolicyReason,
    ) void {
        ctx.policy_action = action;
        ctx.policy_reason = reason;
    }

    /// The matcher's verdict, copied out of the buffers it borrows.
    fn applyDecision(ctx: *Context, snapshot: *const matcher.Snapshot, decision: matcher.Decision) void {
        ctx.notePolicy(
            if (decision.blocked) .block else .allow,
            provenance.fromMatcherReason(decision.reason),
        );
        copyInto(&ctx.matched_buf, &ctx.matched_len, decision.matched);
        if (decision.source) |index| {
            ctx.source_id = snapshot.sources[index].id;
            ctx.source_name = snapshot.sources[index].name;
        }
    }

    fn setCnameTarget(ctx: *Context, target: name.Name) void {
        var text: [types.max_name_len]u8 = undefined;
        copyInto(&ctx.cname_buf, &ctx.cname_len, matcher.normalize(target, &text));
    }

    fn setSafeSearchTarget(ctx: *Context, target: name.Name) void {
        var text: [types.max_name_len]u8 = undefined;
        copyInto(&ctx.safe_search_buf, &ctx.safe_search_len, matcher.normalize(target, &text));
    }

    /// The pool's answering resolver. It is operator-supplied text that may
    /// carry a credential, so it is redacted here — before `Entry.init` copies
    /// it — and never stored raw.
    fn setUpstreamRedacted(ctx: *Context, identity: []const u8) void {
        var w: std.Io.Writer = .fixed(&ctx.upstream_buf);
        // `logger_mod.max_upstream_len` is at least `safe_url.redact`'s own
        // output bound, so the buffer cannot run short.
        w.print("{f}", .{safe_url.redact(identity)}) catch unreachable;
        ctx.upstream_len = @intCast(w.buffered().len);
    }

    /// A forward zone's resolver, spelled the way `validate.parseResolver`
    /// accepts it, so a row names the configured value.
    fn setUpstreamResolver(ctx: *Context, resolver: validate.Resolver) void {
        var w: std.Io.Writer = .fixed(&ctx.upstream_buf);
        w.writeAll(switch (resolver.scheme) {
            .udp => "udp://",
            .tcp => "tcp://",
        }) catch unreachable;

        const bracketed = switch (resolver.addr) {
            .ip4 => false,
            .ip6 => true,
        };
        if (bracketed) w.writeByte('[') catch unreachable;
        resolver.addr.format(&w) catch unreachable;
        if (bracketed) w.writeByte(']') catch unreachable;
        w.print(":{d}", .{resolver.port}) catch unreachable;
        ctx.upstream_len = @intCast(w.buffered().len);
    }

    /// PLAN §6 in order. Local records win over forward zones (ruling 6), and
    /// both win over filtering: a name nxdns answers itself never reaches a
    /// blocklist.
    fn run(ctx: *Context) Handler.Outcome {
        if (ctx.q.qclass != .in) {
            ctx.notePolicy(.not_evaluated, .non_in_class);
            return ctx.viaUpstream(.{ .filter = false, .cache = false });
        }
        if (ctx.records.hasName(ctx.domain)) return ctx.viaLocal();
        if (ctx.zones.match(ctx.domain)) |zone| return ctx.viaForwardZone(zone);

        const paused = if (ctx.handler.pause) |p| p.isPaused(ctx.now_s) else false;
        if (paused) {
            bump(&ctx.handler.stats.paused_queries);
            ctx.notePolicy(.not_evaluated, .paused);
        } else if (ctx.snapshot == null) {
            ctx.notePolicy(.not_evaluated, .snapshot_unavailable);
        }
        return ctx.viaUpstream(.{ .filter = !paused, .cache = true });
    }

    /// PLAN §6.4. A name that exists with no record of the queried type is
    /// NODATA, not a forward: nxdns owns the name either way.
    ///
    /// A local CNAME is returned as it stands (ruling 12). The client re-queries
    /// the target, and that query runs the whole pipeline.
    fn viaLocal(ctx: *Context) Handler.Outcome {
        ctx.notePolicy(.allow, .local_record);
        ctx.route_kind = .local;
        const found = ctx.records.lookup(ctx.domain, ctx.q.qtype);

        var b = packet.ResponseBuilder.init(ctx.response_buf, ctx.hdr, ctx.q) catch
            return ctx.servFail();
        b.setAuthoritative(true);
        records.writeAnswers(&b, ctx.q.name, found) catch return ctx.servFail();
        if (ctx.opt) |o| b.addOptEcho(o, ctx.do_bit) catch return ctx.servFail();

        bump(&ctx.handler.stats.local_answers);
        return ctx.reply(b.finish());
    }

    /// PLAN §6.5. The zone's resolver answers, and filtering, safe search and
    /// uncloaking are all bypassed (ruling 7): a conditional forward exists to
    /// reach a box on the LAN, and a blocklist entry must not stand between the
    /// two.
    fn viaForwardZone(ctx: *Context, zone: *const forward_zones.Zone) Handler.Outcome {
        ctx.notePolicy(.allow, .forward_zone);
        ctx.route_kind = .forward_zone;
        ctx.forward_zone = zone.zone;

        const key = dns_cache.buildKey(
            &ctx.scratch.key,
            ctx.domain,
            @intFromEnum(ctx.q.qtype),
            @intFromEnum(ctx.q.qclass),
            ctx.do_bit,
            null,
        );
        if (ctx.cacheGet(key)) |hit| {
            ctx.route_kind = .cache;
            return ctx.reply(hit);
        }

        // Named before the exchange, so a resolver that fails still appears on
        // the SERVFAIL row it produces.
        ctx.setUpstreamResolver(zone.resolver);

        var client: forward_client.ForwardClient = .init(
            zone.resolver,
            &ctx.scratch.frame,
            ctx.policy.forward_read_timeout,
        );
        // The client lives on this query's stack, so its counters have to move
        // into the handler's before it goes out of scope — on the failure path
        // too, which is the one `forward_failures` exists for.
        defer foldForwardStats(&ctx.handler.stats, client.stats);

        const answer = client.exchange(ctx.io, ctx.query, ctx.response_buf) catch |err| {
            return switch (transport.group(err)) {
                .cancellation => .drop,
                // A budget that ran out is SERVFAIL like any other failure: no
                // rcode says "I gave up in time". It is not logged per query —
                // `nxdns_upstream_budget_exhausted_total` is the record.
                .peer_fault, .local_resource, .budget_exhausted => ctx.servFail(),
            };
        };

        bump(&ctx.handler.stats.forward_zone_answers);
        ctx.cachePut(key, answer);
        return ctx.reply(answer);
    }

    /// Filtering, safe search, the cache and the upstream pool.
    fn viaUpstream(ctx: *Context, mode: Mode) Handler.Outcome {
        var target: ?name.Name = null;
        if (mode.filter) if (ctx.snapshot) |s| {
            const decision = s.evaluate(ctx.group, ctx.domain);
            ctx.applyDecision(s, decision);
            if (decision.blocked) return ctx.viaBlocked(false);
            if (s.safeSearch(ctx.group)) target = safesearch.rewrite(ctx.domain);
        };

        const outgoing = ctx.outgoingQuery(target) orelse return ctx.servFail();
        if (target) |t| {
            bump(&ctx.handler.stats.safesearch_rewrites);
            // A rewrite is not a policy decision: the answer is still allowed,
            // and `safe_search_target` is what says the question changed.
            ctx.setSafeSearchTarget(t);
        }

        // Ruling 5: a safe-search answer is not cached. It would have to be
        // stored under the rewritten name, and every hit would then have to
        // re-encode the answer records under the original one.
        const cacheable = mode.cache and target == null and outgoing.cacheable;
        const key: ?[]const u8 = if (cacheable) ctx.cacheKey() else null;
        if (key) |k| if (ctx.cacheGet(k)) |hit| {
            ctx.route_kind = .cache;
            return ctx.reply(hit);
        };

        // The identity survives failure: `transport.Client` names the resolver
        // it is about to attempt, so a SERVFAIL row says which one lost. It is
        // borrowed from the generation's endpoint (transport.zig:350), so it is
        // redacted into this query's own buffer before the release below —
        // `Handler` is shared across concurrent queries and has no buffer of
        // its own to put it in.
        var selected: ?[]const u8 = null;
        const answer = exchanged: {
            const generation = ctx.handler.upstream.acquire(ctx.io);
            defer ctx.handler.upstream.release(ctx.io, generation);

            const result = generation.client.exchange(
                ctx.io,
                outgoing.bytes,
                ctx.response_buf,
                &selected,
            );
            if (selected) |identity| ctx.setUpstreamRedacted(identity);

            break :exchanged result catch |err| switch (transport.group(err)) {
                // The process is shutting down. There is nothing to say, and
                // the client is about to lose the socket anyway; the listener's
                // own counters record the abandoned datagram.
                .cancellation => return .drop,
                // A budget that ran out is SERVFAIL like any other failure: no
                // rcode says "I gave up in time". It is not logged per query —
                // `nxdns_upstream_budget_exhausted_total` is the record — and
                // the pool leaves `selected` unchanged, so the row still names
                // the last attributable endpoint if there was one, and names
                // none only when no attributable attempt happened.
                .peer_fault, .local_resource, .budget_exhausted => return ctx.servFail(),
            };
        };
        bump(&ctx.handler.stats.queries);

        // Ruling 11. A safe-search answer is exempt: its records belong to the
        // provider's target name, which the client never asked about.
        if (mode.filter and target == null) if (ctx.snapshot) |s| {
            if (ctx.uncloak(s, answer)) |uncloaked| {
                ctx.setCnameTarget(uncloaked.target);
                ctx.applyDecision(s, uncloaked.decision);
                return ctx.viaBlocked(true);
            }
        };

        const final = if (target) |t|
            (ctx.safeSearchReply(t, answer) orelse return ctx.servFail())
        else
            answer;

        if (key) |k| ctx.cachePut(k, final);
        return ctx.reply(final);
    }

    /// PLAN §6.2. `uncloaked` distinguishes a name blocked in its own right
    /// from one blocked through its CNAME chain; either way the reply answers
    /// the question the client asked.
    fn viaBlocked(ctx: *Context, uncloaked: bool) Handler.Outcome {
        // The policy fields already describe the decision that got here: for an
        // uncloaked block they describe the target's own decision, and the
        // non-null `cname_target` is what says a chain was followed. There is
        // no `cname:` reason prefix any more.
        ctx.route_kind = .blocked;
        // Nothing was fetched for the client, so no upstream is named even
        // though the answer that revealed the chain came from one.
        ctx.upstream_len = 0;

        const bytes = response.writeBlocked(
            ctx.response_buf,
            ctx.hdr,
            ctx.q,
            ctx.opt,
            ctx.do_bit,
            ctx.policy.blocking,
        ) catch return ctx.servFail();

        bump(if (uncloaked) &ctx.handler.stats.uncloak_blocked else &ctx.handler.stats.blocked);
        return ctx.reply(bytes);
    }

    /// S3.2 reverses ruling 20's silence: a SERVFAIL the client sees is a row
    /// like any other, carrying the policy and route the pipeline had reached
    /// and — when the failure came from an exchange — the resolver that lost.
    fn servFail(ctx: *Context) Handler.Outcome {
        bump(&ctx.handler.stats.servfail);
        return ctx.reply(build(
            ctx.hdr,
            ctx.q,
            ctx.opt,
            @intFromEnum(types.Rcode.serv_fail),
            false,
            ctx.response_buf,
        ));
    }

    /// The last step of every answered path: the UDP size check, then the query
    /// log row. `bytes` is always a prefix of `response_buf`.
    fn reply(ctx: *Context, bytes: []u8) Handler.Outcome {
        var message = bytes;
        if (ctx.which == .udp and message.len > udpLimit(ctx.p)) {
            // The answer occupies `response_buf`, so the replacement is built
            // beside it and copied over. Its RCODE is the one the answer
            // carried: rebuilding as NOERROR would hand the client an oversized
            // NXDOMAIN as a success, and the twelve-bit form has to be split
            // across the header and the reply's OPT to survive at all.
            bump(&ctx.handler.stats.truncated);
            var scratch: [udp_limit_min]u8 = undefined;
            const truncated = build(ctx.hdr, ctx.q, ctx.opt, messageRcode(message), true, &scratch);
            @memcpy(ctx.response_buf[0..truncated.len], truncated);
            message = ctx.response_buf[0..truncated.len];
        }
        ctx.log(message);
        return .{ .reply = message };
    }

    /// The one place a query becomes a row. Every provenance field is read off
    /// the context, and the RCODE off the packet the client is about to
    /// receive, so no path has to carry a "known rcode" of its own.
    fn log(ctx: *Context, message: []const u8) void {
        const sink = ctx.handler.sink orelse return;

        var ip_buf: [logger_mod.max_client_len]u8 = undefined;
        var w: std.Io.Writer = .fixed(&ip_buf);
        ctx.from.format(&w) catch unreachable;

        const now = std.Io.Clock.real.now(ctx.io);
        sink.log(ctx.io, logger_mod.Entry.init(.{
            .timestamp = ctx.now_s,
            .domain = ctx.domain,
            .client_ip = w.buffered(),
            .qtype = @intFromEnum(ctx.q.qtype),
            .qclass = ctx.qclass,
            .rcode = messageRcode(message),
            .blocked = ctx.policy_action == .block,
            .response_time_us = now.toMicroseconds() - ctx.started.toMicroseconds(),
            .cache_hit = ctx.cacheHit(),
            .upstream = ctx.upstream_buf[0..ctx.upstream_len],
            .group_id = ctx.group_id,
            .group_name = ctx.group_name,
            .policy_action = ctx.policy_action,
            .policy_reason = ctx.policy_reason,
            .matched = ctx.matched_buf[0..ctx.matched_len],
            .source_id = ctx.source_id,
            .source_name = ctx.source_name,
            .cname_target = ctx.cname_buf[0..ctx.cname_len],
            .safe_search_target = ctx.safe_search_buf[0..ctx.safe_search_len],
            .route_kind = ctx.route_kind,
            .forward_zone = ctx.forward_zone,
        }));
    }

    /// Whether the cache answered, or null where it never applied: a local
    /// record, a blocked answer and a protocol refusal all bypass it entirely,
    /// and "false" would claim a lookup that never happened (ruling 20).
    fn cacheHit(ctx: *const Context) ?bool {
        return switch (ctx.route_kind) {
            .cache => true,
            .upstream, .forward_zone => false,
            .local, .blocked, .rejected => null,
        };
    }

    /// The cache key for this query, or null when it must not be cached.
    fn cacheKey(ctx: *Context) ?[]const u8 {
        var ecs: ?[]const u8 = null;
        if (ctx.policy.ecs_mode == .forward) {
            if (ctx.opt) |o| switch (subnetForKey(ctx.query, o)) {
                .none => {},
                .subnet => |payload| ecs = payload,
                .uncacheable => return null,
            };
        }
        return dns_cache.buildKey(
            &ctx.scratch.key,
            ctx.domain,
            @intFromEnum(ctx.q.qtype),
            @intFromEnum(ctx.q.qclass),
            ctx.do_bit,
            ecs,
        );
    }

    fn cacheGet(ctx: *Context, key: []const u8) ?[]u8 {
        // The pointer is read under the mutex, not before it: `replaceCache`
        // swaps it under the same lock, and a load taken outside would hand
        // this query the cache the swap has just freed.
        ctx.handler.cache_mutex.lockUncancelable(ctx.io);
        const hit = if (ctx.handler.cache) |cache|
            cache.get(ctx.now_s, key, ctx.response_buf)
        else
            null;
        ctx.handler.cache_mutex.unlock(ctx.io);

        const bytes = hit orelse return null;
        bump(&ctx.handler.stats.cache_hits);
        packet.setId(bytes, ctx.hdr.id);
        return bytes;
    }

    /// A response the cache declines to hold, or an allocation it cannot make,
    /// is not a query failure: the answer already went out. `DnsCache.stats`
    /// carries what happened.
    fn cachePut(ctx: *Context, key: []const u8, message: []const u8) void {
        const class = dns_cache.classify(message, ctx.policy.negative_ttl_max) orelse return;

        ctx.handler.cache_mutex.lockUncancelable(ctx.io);
        defer ctx.handler.cache_mutex.unlock(ctx.io);
        const cache = ctx.handler.cache orelse return;
        cache.put(ctx.now_s, key, message, class) catch {};
    }

    /// The bytes to send upstream. Null means the rewrite did not fit and the
    /// caller answers SERVFAIL — which only a safe-search rewrite can hit,
    /// because ruling 13 sends the original query when an ECS strip overflows.
    fn outgoingQuery(ctx: *Context, target: ?name.Name) ?Outgoing {
        const t = target orelse {
            if (ctx.policy.ecs_mode != .strip) return .{ .bytes = ctx.query };
            const opt = ctx.opt orelse return .{ .bytes = ctx.query };
            return ctx.stripEcs(ctx.query, ctx.p, opt, &ctx.scratch.build);
        };

        const rewritten = ctx.rewriteQuestion(t) orelse return null;
        if (ctx.policy.ecs_mode != .strip) return .{ .bytes = rewritten };

        // The question rewrite moved the OPT record, so the ECS option comes
        // out of the rebuilt query rather than the client's.
        const p = packet.parse(rewritten) catch return null;
        const rec = packet.findOptRecord(p) orelse return .{ .bytes = rewritten };
        const opt = edns.parseOpt(rewritten, rec) catch return .{ .bytes = rewritten };
        return ctx.stripEcs(rewritten, p, opt, &ctx.scratch.synth);
    }

    /// Ruling 13: a query whose stripped form does not fit is forwarded as it
    /// arrived, with the subnet still in it, and the counter says so. Dropping
    /// the query instead would take the name off the air over a header nobody
    /// asked for.
    fn stripEcs(
        ctx: *Context,
        query: []const u8,
        p: packet.Packet,
        opt: edns.OptRecord,
        out: []u8,
    ) Outgoing {
        const stripped = edns.stripEcs(query, p, opt, out) catch {
            bump(&ctx.handler.stats.ecs_strip_failed);
            // The subnet reached the resolver after all, so the answer may be
            // specific to it while the key — built for `.strip` mode — says
            // nothing about a subnet. Caching it would file a subnet-specific
            // answer under the name every other client asks for, and reading
            // the cache would answer this query from an entry that ignored the
            // subnet. Neither direction is safe, so this query skips the cache.
            return .{ .bytes = query, .cacheable = false };
        };
        return switch (stripped) {
            .unchanged => .{ .bytes = query },
            .rewritten => |bytes| .{ .bytes = bytes },
        };
    }

    /// The client's query with the safe-search target as its question name.
    ///
    /// The header counts are written fresh, so no record besides the OPT
    /// survives: a name in one of them may be a compression pointer into the
    /// question, and the question just changed length. A query carrying such a
    /// record is already outside RFC 1035 §4.1.2, and the alternative is
    /// forwarding a name that no longer decodes.
    fn rewriteQuestion(ctx: *Context, target: name.Name) ?[]const u8 {
        var w: std.Io.Writer = .fixed(&ctx.scratch.build);

        var hdr = ctx.hdr;
        hdr.qdcount = 1;
        hdr.ancount = 0;
        hdr.nscount = 0;
        hdr.arcount = if (ctx.opt == null) 0 else 1;
        var encoded: [types.header_len]u8 = undefined;
        header.encode(hdr, &encoded);

        w.writeAll(&encoded) catch return null;
        question.encode(
            .{ .name = target, .qtype = ctx.q.qtype, .qclass = ctx.q.qclass },
            &w,
        ) catch return null;
        if (ctx.opt) |o| edns.encodeOpt(o, o.options.slice(ctx.query), &w) catch return null;
        return w.buffered();
    }

    /// Ruling 10: the client asked about the original name, so the reply keeps
    /// the original question, states the CNAME to the target, and then carries
    /// the target's address records verbatim. Every other rtype is dropped —
    /// their RDATA can hold compression pointers into the upstream message,
    /// which would not decode inside this one.
    fn safeSearchReply(ctx: *Context, target: name.Name, answer: []const u8) ?[]u8 {
        const p = packet.parse(answer) catch return null;

        // The synthesized CNAME lives exactly as long as the addresses it
        // points at. With no address record there is nothing to hold, and a
        // zero TTL says so.
        var ttl: u32 = 0;
        var measure = packet.answers(p);
        var seen = false;
        while (measure.next() catch return null) |rec| {
            if (rec.rtype != .a and rec.rtype != .aaaa) continue;
            ttl = if (seen) @min(ttl, rec.ttl) else rec.ttl;
            seen = true;
        }

        var b = packet.ResponseBuilder.init(&ctx.scratch.synth, ctx.hdr, ctx.q) catch return null;
        b.setRcode(p.header.flags.rcode);
        b.addAnswer(ctx.q.name, .cname, .in, ttl, target.wire()) catch return null;

        var it = packet.answers(p);
        while (it.next() catch return null) |rec| {
            switch (rec.rtype) {
                .a, .aaaa => b.addAnswer(
                    target,
                    rec.rtype,
                    .in,
                    rec.ttl,
                    rec.rdata.slice(answer),
                ) catch return null,
                else => {},
            }
        }
        if (ctx.opt) |o| b.addOptEcho(o, ctx.do_bit) catch return null;

        const message = b.finish();
        if (message.len > ctx.response_buf.len) return null;
        @memcpy(ctx.response_buf[0..message.len], message);
        return ctx.response_buf[0..message.len];
    }

    /// PLAN §6.3: walk the answer section's CNAME chain and evaluate every
    /// target for the same group. Nothing is re-resolved — the chain is read out
    /// of the answer that already arrived. The reason of the first blocked
    /// target is returned; null means the chain ended, looped past
    /// `max_cname_depth`, or held nothing blocked.
    ///
    /// The full decision comes back, not just its reason: the row explains the
    /// block with the target's own verdict, matched rule and source.
    fn uncloak(ctx: *Context, snapshot: *const matcher.Snapshot, answer: []const u8) ?Uncloaked {
        const p = packet.parse(answer) catch return null;

        var current = ctx.q.name;
        var depth: usize = 0;
        while (depth < max_cname_depth) : (depth += 1) {
            const next = cnameTarget(p, current) orelse return null;
            const decision = snapshot.evaluate(
                ctx.group,
                matcher.normalize(next, &ctx.scratch.uncloak),
            );
            // `decision.matched` may borrow from `scratch.uncloak`, which the
            // next iteration overwrites, so the caller copies before continuing.
            if (decision.blocked) return .{ .decision = decision, .target = next };
            current = next;
        }
        return null;
    }
};

/// The CNAME chain step that decided the query, and the name it named.
const Uncloaked = struct {
    decision: matcher.Decision,
    target: name.Name,
};

/// The query as it goes upstream, and whether the cache may describe it.
const Outgoing = struct {
    bytes: []const u8,
    /// False when the subnet reaching the resolver is not the subnet the key
    /// would state. The cache is then skipped in both directions: a hit would
    /// answer from an entry built for a different subnet, and a store would
    /// hand this client's answer to every other client.
    cacheable: bool = true,
};

/// What the cache key says about the client's subnet in `.forward` mode.
const SubnetKey = union(enum) {
    /// The query carries no subnet, so the key states none.
    none,
    subnet: []const u8,
    uncacheable,
};

/// The subnet for the cache key, or `.uncacheable` when no single value
/// describes the query.
///
/// RFC 7871 §6 allows one ECS option, but a query is client-controlled and can
/// carry several. The whole OPT record is forwarded in `.forward` mode, so a
/// resolver honouring the second option would answer for a subnet the key never
/// mentioned — and the entry would then serve every client whose first option
/// matched. Keying on the first option is what makes that reachable, so a
/// repeated option is not cached at all.
///
/// A payload longer than the key holds is refused for the same reason:
/// truncating it would merge two subnets into one entry (PLAN §8).
fn subnetForKey(query: []const u8, opt: edns.OptRecord) SubnetKey {
    var found: ?[]const u8 = null;
    var it = edns.options(query, opt);
    // `parseOpt` already walked this list, so the error is unreachable for an
    // `OptRecord` this file produced; refusing the cache is the safe direction
    // for one assembled by hand.
    while (it.next() catch return .uncacheable) |option| {
        if (option.code != edns.ecs_option_code) continue;
        if (found != null) return .uncacheable;
        found = option.data;
    }

    const payload = found orelse return .none;
    if (payload.len > dns_cache.max_ecs_len) return .uncacheable;
    return .{ .subnet = payload };
}

/// Copies as much of `value` as `buf` holds and stores the length. The same
/// helper serves the `u8` lengths and the `u16` one, because `buf.len` is what
/// bounds the copy.
fn copyInto(buf: []u8, len: anytype, value: []const u8) void {
    const n = @min(buf.len, value.len);
    @memcpy(buf[0..n], value[0..n]);
    len.* = @intCast(n);
}

/// The full twelve-bit RCODE of an encoded message, OPT record included. A
/// message that will not re-parse still has a readable header, and a header
/// that will not parse is not a message this file produced.
fn messageRcode(message: []const u8) u12 {
    const p = packet.parse(message) catch {
        const hdr = header.parse(message) catch return 0;
        return @intFromEnum(hdr.flags.rcode);
    };
    const rec = packet.findOptRecord(p) orelse
        return @intFromEnum(p.header.flags.rcode);
    const opt = edns.parseOpt(message, rec) catch
        return @intFromEnum(p.header.flags.rcode);
    return edns.extendedRcode(p.header.flags.rcode, opt);
}

/// The target of the answer-section CNAME owned by `owner`, or null when the
/// chain stops there.
fn cnameTarget(p: packet.Packet, owner: name.Name) ?name.Name {
    var it = packet.answers(p);
    while (it.next() catch return null) |rec| {
        if (rec.rtype != .cname) continue;
        if (!name.eqlIgnoreCase(rec.name, owner)) continue;
        return record.rdataCname(p.bytes, rec) catch null;
    }
    return null;
}

/// Counts one synthesized reply and encodes it. The counter is passed in
/// because the counter *is* the record that this failure mode happened.
fn synthesize(
    hdr: header.Header,
    q: ?question.Question,
    opt: ?edns.OptRecord,
    rcode: types.Rcode,
    counter: *std.atomic.Value(u64),
    response_buf: []u8,
) Handler.Outcome {
    bump(counter);
    return .{ .reply = build(hdr, q, opt, @intFromEnum(rcode), false, response_buf) };
}

/// Whether the section holds a record of TYPE=OPT. A section that will not walk
/// counts as holding one: the caller's only answer to either condition is
/// FORMERR, and a `Packet` from `packet.parse` walks in full, so the fallback is
/// reachable only for a `Packet` assembled by hand around unvalidated bytes.
fn containsOpt(section: packet.RecordIterator) bool {
    var it = section;
    while (it.next() catch return true) |rec| {
        if (rec.rtype == .opt) return true;
    }
    return false;
}

/// The query's advertised UDP payload size, clamped to `[512, 4096]`, or 512
/// when the query carries no usable OPT record.
pub fn udpLimit(query_packet: packet.Packet) u16 {
    const rec = packet.findOptRecord(query_packet) orelse return udp_limit_min;
    const opt = edns.parseOpt(query_packet.bytes, rec) catch return udp_limit_min;
    return std.math.clamp(opt.udp_payload_size, udp_limit_min, udp_limit_max);
}

/// Encodes one synthesized reply. `ResponseBuilder.init` copies the request's
/// ID, opcode and RD bit and sets QR and RA, so every reply built here binds to
/// its request. `buf` is at least `udp_limit_min`, which `max_synthetic_len`
/// proves is enough, so the writer cannot fail.
///
/// `rcode` is the full twelve bits (RFC 6891 §6.1.3), split across the header
/// and the reply's OPT record. A code above 15 — BADVERS, or an extended code
/// preserved from an answer being truncated — therefore needs an OPT record to
/// survive: without one the upper eight bits have nowhere to go.
fn build(
    hdr: header.Header,
    q: ?question.Question,
    opt: ?edns.OptRecord,
    rcode: u12,
    tc: bool,
    buf: []u8,
) []u8 {
    const split = edns.splitRcode(rcode);
    var b = packet.ResponseBuilder.init(buf, hdr, q) catch unreachable;
    b.setRcode(split.header);
    b.header.flags.tc = tc;
    // An EDNS query gets an EDNS reply, and the DO bit passes through
    // untouched — nxdns validates no signatures, so it must not claim the
    // client asked for none (PLAN §6.1).
    if (opt) |o| b.addOptWithRcode(o, o.do_bit, split.extended) catch unreachable;
    return b.finish();
}

fn bump(counter: *std.atomic.Value(u64)) void {
    _ = counter.fetchAdd(1, .monotonic);
}

/// Moves one forward-zone exchange's counters into the handler's. `stats` is a
/// plain per-instance struct and stays that way (milestone-16 ruling 14); this
/// is the one place it becomes a process-wide number.
fn foldForwardStats(into: *Handler.Stats, from: forward_client.ForwardClient.Stats) void {
    _ = into.forward_udp_truncated.fetchAdd(from.udp_truncated, .monotonic);
    _ = into.forward_foreign_datagrams.fetchAdd(from.foreign_datagrams, .monotonic);
    _ = into.forward_failures.fetchAdd(from.failures, .monotonic);
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

const sse = @import("../web/sse.zig");

const testing = std.testing;

/// Every test needs a real `std.Io`: the handler reads the clock on every query
/// and takes a mutex for every collaborator that is wired in.
const TestIo = struct {
    threaded: std.Io.Threaded,

    fn init() TestIo {
        return .{ .threaded = .init(testing.allocator, .{}) };
    }

    fn io(self: *TestIo) std.Io {
        return self.threaded.io();
    }

    fn deinit(self: *TestIo) void {
        self.threaded.deinit();
    }
};

const blocking: response.Options = .{ .mode = .zero, .ttl = 5 };
const forward_timeout: std.Io.Clock.Duration = .{ .raw = .fromMilliseconds(50), .clock = .awake };
const client_ip: address.NetAddress = .{ .ip4 = .{ 192, 168, 1, 50 } };

/// The handler the check command and the pre-Phase-7 listeners build: an
/// upstream and nothing else optional.
fn bare(up: *upstream_owner.Owner) Handler {
    return .{
        .upstream = up,
        .policy = .{ .blocking = blocking, .forward_read_timeout = forward_timeout },
    };
}

/// One query with a fresh scratch. The reply is always a prefix of `buf`, so it
/// outlives the scratch this frame owns.
fn handleQuery(
    h: *Handler,
    io: std.Io,
    which: Transport,
    from: address.NetAddress,
    query: []const u8,
    buf: []u8,
) Handler.Outcome {
    var scratch: Scratch = undefined;
    return h.handle(io, which, from, query, buf, &scratch);
}

fn udp(h: *Handler, io: std.Io, query: []const u8, buf: []u8) Handler.Outcome {
    return handleQuery(h, io, .udp, client_ip, query, buf);
}

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

/// The matching response: the question echoed plus one A record.
const response_bytes =
    "\x12\x34\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00" ++
    "\x07example\x03com\x00\x00\x01\x00\x01" ++
    "\xc0\x0c\x00\x01\x00\x01\x00\x00\x01\x2c\x00\x04\x5d\xb8\xd8\x22";

const opt_len = 11;
const query_with_opt_len = query_bytes.len + opt_len;

/// The EDNS version byte, counted from the start of the OPT record: past the
/// root owner name, TYPE, CLASS and the extended-RCODE byte.
const opt_version_offset = 6;

/// `query_bytes` plus an OPT record advertising `payload_size`, EDNS version 0.
fn queryWithOpt(buf: *[query_with_opt_len]u8, payload_size: u16, do_bit: bool) []const u8 {
    return queryWithOptVersion(buf, payload_size, do_bit, 0);
}

/// `queryWithOpt` for a chosen EDNS version.
fn queryWithOptVersion(
    buf: *[query_with_opt_len]u8,
    payload_size: u16,
    do_bit: bool,
    version: u8,
) []const u8 {
    @memcpy(buf[0..query_bytes.len], query_bytes);
    std.mem.writeInt(u16, buf[10..12], 1, .big); // arcount

    const opt = buf[query_bytes.len..][0..opt_len];
    @memset(opt, 0);
    opt[2] = @intFromEnum(types.Type.opt);
    std.mem.writeInt(u16, opt[3..5], payload_size, .big);
    opt[opt_version_offset] = version;
    if (do_bit) opt[7] = 0x80; // the DO bit is bit 15 of the TTL word
    return buf;
}

/// `query_bytes` plus an OPT record whose owner name is `com.` instead of the
/// root, which RFC 6891 §6.1.1 forbids.
const query_with_named_opt =
    "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
    "\x07example\x03com\x00\x00\x01\x00\x01" ++
    "\x03com\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";

/// `query_bytes` plus an OPT record whose RDATA ends in a three-byte option
/// header. The record itself fits the packet; only the option list is broken.
const query_with_bad_option =
    "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
    "\x07example\x03com\x00\x00\x01\x00\x01" ++
    "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x03\x00\x08\x00";

/// A bare OPT record: root owner, TYPE 41, 4096-byte payload, empty RDATA.
const opt_record = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";

/// `query_bytes` with the OPT record counted into the answer section, which
/// RFC 6891 §6.1.1 forbids.
const query_with_opt_in_answer =
    "\x12\x34\x01\x00\x00\x01\x00\x01\x00\x00\x00\x00" ++
    "\x07example\x03com\x00\x00\x01\x00\x01" ++
    opt_record;

/// The same OPT record, counted into the authority section instead.
const query_with_opt_in_authority =
    "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x01\x00\x00" ++
    "\x07example\x03com\x00\x00\x01\x00\x01" ++
    opt_record;

/// Two OPT records in the additional section. RFC 6891 §6.1.1 allows one.
const query_with_two_opts =
    "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x02" ++
    "\x07example\x03com\x00\x00\x01\x00\x01" ++
    opt_record ++ opt_record;

const FakeUpstream = struct {
    reply: []const u8 = &.{},
    err: ?transport.ExchangeError = null,
    /// What the pool would report as the resolver it attempted. A real one is
    /// operator-supplied text, so a test can put a credential in it.
    identity: []const u8 = "fake://handler-upstream",
    calls: usize = 0,
    /// The last query the handler sent, so a test can assert what the rewrite
    /// stages produced.
    seen: [1024]u8 = undefined,
    seen_len: usize = 0,

    fn exchangeFn(
        ptr: *anyopaque,
        io: std.Io,
        query: []const u8,
        response_buf: []u8,
        selected: *?[]const u8,
    ) transport.ExchangeError![]u8 {
        _ = io;
        const self: *FakeUpstream = @ptrCast(@alignCast(ptr));
        // Named before the attempt, so the identity survives the failure below.
        selected.* = self.identity;
        self.calls += 1;
        self.seen_len = @min(query.len, self.seen.len);
        @memcpy(self.seen[0..self.seen_len], query[0..self.seen_len]);
        if (self.err) |e| return e;
        if (self.reply.len > response_buf.len) return error.ResponseTooLarge;
        @memcpy(response_buf[0..self.reply.len], self.reply);
        const bytes = response_buf[0..self.reply.len];
        if (bytes.len >= types.header_len) {
            packet.setId(bytes, (header.parse(query) catch unreachable).id);
        }
        return bytes;
    }

    fn client(self: *FakeUpstream) transport.Client {
        return .{ .ptr = self, .exchangeFn = exchangeFn };
    }

    fn sent(self: *const FakeUpstream) []const u8 {
        return self.seen[0..self.seen_len];
    }
};

fn expectReply(outcome: Handler.Outcome) ![]u8 {
    return switch (outcome) {
        .reply => |bytes| bytes,
        .drop => error.TestUnexpectedDrop,
    };
}

test "a udp query is forwarded and the upstream reply is returned unchanged" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    try testing.expectEqualSlices(u8, response_bytes, reply);
    try testing.expectEqual(@as(usize, 1), fake.calls);
    try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.truncated.load(.monotonic));
    // A query that carries no OPT at all is not an EDNS violation.
    try testing.expectEqual(@as(u64, 0), h.stats.formerr.load(.monotonic));
}

test "a response arriving on the listener port is dropped" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    try testing.expectEqual(Handler.Outcome.drop, udp(&h, t.io(), response_bytes, &buf));
    try testing.expectEqual(@as(u64, 1), h.stats.dropped_malformed.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "a query shorter than a header is dropped" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    try testing.expectEqual(Handler.Outcome.drop, udp(&h, t.io(), query_bytes[0..8], &buf));
    try testing.expectEqual(@as(u64, 1), h.stats.dropped_malformed.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "a query whose question runs off the end gets FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    // The header promises a question; the packet ends after 12 bytes plus a
    // partial name, which `packet.parse` reports as a section overrun.
    var buf: [udp_limit_min]u8 = undefined;
    const overrun = query_bytes[0 .. query_bytes.len - 3];
    const reply = try expectReply(udp(&h, t.io(), overrun, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
}

test "a query with QDCOUNT 0 gets FORMERR with no question" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    const no_question = "\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00";
    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), no_question, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(@as(u16, 0), p.header.qdcount);
    try testing.expectEqual(true, p.header.flags.qr);
    try testing.expectEqual(true, p.header.flags.ra);
    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "a query with QDCOUNT 2 gets FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    const two =
        "\x12\x34\x01\x00\x00\x02\x00\x00\x00\x00\x00\x00" ++
        "\x07example\x03com\x00\x00\x01\x00\x01" ++
        "\x07example\x03com\x00\x00\x1c\x00\x01";
    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), two, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0), p.header.qdcount);
    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "a structurally broken question gets FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    // A compression pointer to itself: the name never terminates.
    const loop = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
        "\xc0\x0c\x00\x01\x00\x01";
    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), loop, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(@as(u16, 0), p.header.qdcount);
    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "an unimplemented opcode gets NOTIMP" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var update: [query_bytes.len]u8 = query_bytes.*;
    std.mem.writeInt(u16, update[2..4], 0x2900, .big); // opcode update, RD set

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), &update, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.not_imp, p.header.flags.rcode);
    try testing.expectEqual(types.Opcode.update, p.header.flags.opcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(@as(u16, 0), p.header.qdcount);
    try testing.expectEqual(@as(u64, 1), h.stats.notimp.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "a peer fault becomes SERVFAIL with the question echoed" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .err = error.Timeout };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.serv_fail, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(true, p.header.flags.rd);
    try testing.expectEqual(true, p.header.flags.qr);
    try testing.expectEqual(@as(u16, 1), p.header.qdcount);
    try testing.expectEqual(@as(u16, 0), p.header.ancount);

    const echoed = packet.firstQuestion(p).?;
    const asked = packet.firstQuestion(try packet.parse(query_bytes)).?;
    try testing.expectEqualSlices(u8, asked.name.wire(), echoed.name.wire());
    try testing.expectEqual(types.Type.a, echoed.qtype);

    try testing.expectEqual(@as(u64, 1), h.stats.servfail.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.queries.load(.monotonic));
}

test "a local resource failure becomes SERVFAIL, not a drop" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .err = error.OutOfMemory };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.serv_fail, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 1), p.header.qdcount);
    try testing.expectEqual(@as(u64, 1), h.stats.servfail.load(.monotonic));
}

test "a canceled exchange is dropped" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .err = error.Canceled };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    try testing.expectEqual(Handler.Outcome.drop, udp(&h, t.io(), query_bytes, &buf));
    try testing.expectEqual(@as(u64, 0), h.stats.servfail.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.queries.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.dropped_malformed.load(.monotonic));
}

test "udpLimit clamps the advertised payload size" {
    try testing.expectEqual(@as(u16, 512), udpLimit(try packet.parse(query_bytes)));

    const cases = [_]struct { advertised: u16, expected: u16 }{
        .{ .advertised = 1232, .expected = 1232 },
        .{ .advertised = 200, .expected = 512 },
        .{ .advertised = 9000, .expected = 4096 },
        .{ .advertised = 512, .expected = 512 },
        .{ .advertised = 4096, .expected = 4096 },
    };
    for (cases) |c| {
        var buf: [query_with_opt_len]u8 = undefined;
        const q = queryWithOpt(&buf, c.advertised, false);
        try testing.expectEqual(c.expected, udpLimit(try packet.parse(q)));
    }
}

/// A valid response for `query_bytes` padded past 512 bytes with A records.
fn oversizeResponse(buf: []u8) []u8 {
    return oversizeResponseRcode(buf, @intFromEnum(types.Rcode.no_error), false);
}

/// The same, carrying a chosen twelve-bit RCODE. `with_opt` adds the OPT record
/// an extended code needs to carry its upper eight bits.
fn oversizeResponseRcode(buf: []u8, rcode: u12, with_opt: bool) []u8 {
    const split = edns.splitRcode(rcode);
    const request = packet.parse(query_bytes) catch unreachable;
    const q = packet.firstQuestion(request).?;
    var b = packet.ResponseBuilder.init(buf, request.header, q) catch unreachable;
    b.setRcode(split.header);
    var i: usize = 0;
    while (i < 32) : (i += 1) {
        b.addAnswer(q.name, .a, .in, 300, "\x5d\xb8\xd8\x22") catch unreachable;
    }
    if (with_opt) b.addOptWithRcode(.{
        .udp_payload_size = 1232,
        .extended_rcode = 0,
        .version = 0,
        .do_bit = false,
        .options = .{ .offset = 0, .len = 0 },
    }, false, split.extended) catch unreachable;
    return b.finish();
}

test "an oversize udp reply is replaced by a truncated one" {
    var t: TestIo = .init();
    defer t.deinit();

    var reply_buf: [2048]u8 = undefined;
    const oversize = oversizeResponse(&reply_buf);
    try testing.expect(oversize.len > udp_limit_min);

    var fake: FakeUpstream = .{ .reply = oversize };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [4096]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(true, p.header.flags.tc);
    try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0), p.header.ancount);
    try testing.expectEqual(@as(u16, 1), p.header.qdcount);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(true, p.header.flags.rd);

    const echoed = packet.firstQuestion(p).?;
    const asked = packet.firstQuestion(try packet.parse(query_bytes)).?;
    try testing.expectEqualSlices(u8, asked.name.wire(), echoed.name.wire());

    try testing.expectEqual(@as(u64, 1), h.stats.truncated.load(.monotonic));
    try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
}

test "an oversize NXDOMAIN keeps its rcode when it is replaced by a truncated reply" {
    var t: TestIo = .init();
    defer t.deinit();

    var reply_buf: [2048]u8 = undefined;
    const oversize = oversizeResponseRcode(&reply_buf, @intFromEnum(types.Rcode.nx_domain), false);

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = oversize };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.sink = &sink;

    var buf: [4096]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    // Rebuilding as NOERROR would hand the client an oversized NXDOMAIN as a
    // success, which is what the client acts on.
    const p = try packet.parse(reply);
    try testing.expectEqual(true, p.header.flags.tc);
    try testing.expectEqual(types.Rcode.nx_domain, p.header.flags.rcode);
    try testing.expectEqual(@as(u64, 1), h.stats.truncated.load(.monotonic));

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, t.io(), &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    try testing.expectEqual(@as(u16, @intFromEnum(types.Rcode.nx_domain)), logged[0].rcode);
}

test "a truncated reply preserves a twelve-bit rcode on the wire and in the log" {
    var t: TestIo = .init();
    defer t.deinit();

    // RCODE 20: four bits in the header and one in the OPT record, so nothing
    // short of both halves carries it.
    const extended: u12 = 20;
    var reply_buf: [2048]u8 = undefined;
    const oversize = oversizeResponseRcode(&reply_buf, extended, true);

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = oversize };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.sink = &sink;

    // The client's own OPT advertises 512, so the answer still does not fit.
    var query_buf: [query_with_opt_len]u8 = undefined;
    const query = queryWithOpt(&query_buf, 512, false);

    var buf: [4096]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(true, p.header.flags.tc);
    const opt = try edns.parseOpt(reply, packet.findOptRecord(p).?);
    try testing.expectEqual(extended, edns.extendedRcode(p.header.flags.rcode, opt));
    try testing.expectEqual(@as(u8, 1), opt.extended_rcode);

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, t.io(), &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    try testing.expectEqual(@as(u16, extended), logged[0].rcode);
}

test "the same oversize reply passes through untouched over tcp" {
    var t: TestIo = .init();
    defer t.deinit();

    var reply_buf: [2048]u8 = undefined;
    const oversize = oversizeResponse(&reply_buf);

    var fake: FakeUpstream = .{ .reply = oversize };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [4096]u8 = undefined;
    const reply = try expectReply(handleQuery(&h, t.io(), .tcp, client_ip, query_bytes, &buf));

    try testing.expectEqualSlices(u8, oversize, reply);
    try testing.expectEqual(@as(u64, 0), h.stats.truncated.load(.monotonic));
    try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
}

test "a reply within the advertised EDNS limit is not truncated" {
    var t: TestIo = .init();
    defer t.deinit();

    var reply_buf: [2048]u8 = undefined;
    const oversize = oversizeResponse(&reply_buf);

    var fake: FakeUpstream = .{ .reply = oversize };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var query_buf: [query_with_opt_len]u8 = undefined;
    const query = queryWithOpt(&query_buf, 4096, false);

    var buf: [4096]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query, &buf));
    try testing.expectEqual(oversize.len, reply.len);
    try testing.expectEqual(@as(u64, 0), h.stats.truncated.load(.monotonic));
}

test "the DO bit passes through into a synthesized reply" {
    var t: TestIo = .init();
    defer t.deinit();

    for ([_]bool{ false, true }) |do_bit| {
        var fake: FakeUpstream = .{ .err = error.Timeout };
        var h_owner: upstream_owner.Borrowed = .{};
        var h = bare(h_owner.client(fake.client()));

        var query_buf: [query_with_opt_len]u8 = undefined;
        const query = queryWithOpt(&query_buf, 1232, do_bit);

        var buf: [udp_limit_min]u8 = undefined;
        const reply = try expectReply(udp(&h, t.io(), query, &buf));

        const p = try packet.parse(reply);
        try testing.expectEqual(types.Rcode.serv_fail, p.header.flags.rcode);
        const opt = try edns.parseOpt(reply, packet.findOptRecord(p).?);
        try testing.expectEqual(do_bit, opt.do_bit);
        try testing.expectEqual(@as(u16, 1232), opt.udp_payload_size);
    }
}

test "the truncated reply echoes the OPT record" {
    var t: TestIo = .init();
    defer t.deinit();

    var reply_buf: [2048]u8 = undefined;
    const oversize = oversizeResponse(&reply_buf);

    var fake: FakeUpstream = .{ .reply = oversize };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    // 512 is the advertised size, so the oversize answer still does not fit.
    var query_buf: [query_with_opt_len]u8 = undefined;
    const query = queryWithOpt(&query_buf, 512, true);

    var buf: [4096]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(true, p.header.flags.tc);
    const opt = try edns.parseOpt(reply, packet.findOptRecord(p).?);
    try testing.expectEqual(true, opt.do_bit);
    try testing.expectEqual(@as(u16, 512), opt.udp_payload_size);
    try testing.expectEqual(@as(u64, 1), h.stats.truncated.load(.monotonic));
}

test "an EDNS version this server does not implement gets BADVERS" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var query_buf: [query_with_opt_len]u8 = undefined;
    const query = queryWithOptVersion(&query_buf, 1232, true, 1);

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query, &buf));

    const p = try packet.parse(reply);
    const opt = try edns.parseOpt(reply, packet.findOptRecord(p).?);
    // RCODE 16 lives in neither half alone: the header carries 0 and the OPT
    // record carries 1.
    try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
    try testing.expectEqual(@as(u12, 16), edns.extendedRcode(p.header.flags.rcode, opt));
    try testing.expectEqual(@as(u8, 0), opt.version);
    try testing.expectEqual(true, opt.do_bit);
    try testing.expectEqual(@as(u16, 1232), opt.udp_payload_size);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(true, p.header.flags.qr);
    try testing.expectEqual(@as(u16, 1), p.header.qdcount);
    try testing.expectEqual(@as(u16, 0), p.header.ancount);
    try testing.expectEqual(@as(u64, 1), h.stats.badvers.load(.monotonic));
    // The query never reached the upstream.
    try testing.expectEqual(@as(u64, 0), h.stats.queries.load(.monotonic));
}

test "an EDNS version 0 query is not answered with BADVERS" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var query_buf: [query_with_opt_len]u8 = undefined;
    const query = queryWithOptVersion(&query_buf, 1232, false, 0);

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query, &buf));

    // The query is forwarded and the upstream's own answer comes back, so the
    // BADVERS arm never ran.
    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 1), p.header.ancount);
    try testing.expectEqual(@as(u64, 0), h.stats.badvers.load(.monotonic));
    try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
}

test "a malformed OPT record is FORMERR before the version check reads it" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    // The option list is broken and the EDNS version is 1. `parseOpt` fails
    // first, so nothing trusts the version byte of an OPT that will not parse.
    var query_buf: [query_with_bad_option.len]u8 = (query_with_bad_option ++ "").*;
    query_buf[query_bytes.len + opt_version_offset] = 1;

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), &query_buf, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u64, 0), h.stats.badvers.load(.monotonic));
    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
}

test "an OPT record with a non-root owner name gets FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_with_named_opt, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(true, p.header.flags.qr);
    try testing.expectEqual(true, p.header.flags.ra);

    // The question parsed, so it is echoed; the OPT did not, so none is.
    try testing.expectEqual(@as(u16, 1), p.header.qdcount);
    const echoed = packet.firstQuestion(p).?;
    const asked = packet.firstQuestion(try packet.parse(query_bytes)).?;
    try testing.expectEqualSlices(u8, asked.name.wire(), echoed.name.wire());
    try testing.expect(packet.findOptRecord(p) == null);

    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "an OPT record with a malformed option list gets FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    // The packet as a whole is well-formed: only `parseOpt` rejects it.
    const p_query = try packet.parse(query_with_bad_option);
    try testing.expectError(
        error.BadOption,
        edns.parseOpt(query_with_bad_option, packet.findOptRecord(p_query).?),
    );

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_with_bad_option, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expect(packet.findOptRecord(p) == null);
    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "an OPT record whose rdata runs off the end gets FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    // RDLENGTH claims three bytes and one follows, which `packet.parse` reports
    // as a section overrun before the OPT is ever read.
    const truncated = query_with_bad_option[0 .. query_with_bad_option.len - 2];
    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), truncated, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "an OPT record in the answer section gets FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    // The message walks: only its placement of the OPT is illegal, so the
    // handler's own section walk is what has to reject it.
    const p_query = try packet.parse(query_with_opt_in_answer);
    try testing.expect(packet.findOptRecord(p_query) == null);

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_with_opt_in_answer, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);

    // The question parsed, so it is echoed; the OPT is not echoed anywhere.
    try testing.expectEqual(@as(u16, 1), p.header.qdcount);
    const echoed = packet.firstQuestion(p).?;
    const asked = packet.firstQuestion(try packet.parse(query_bytes)).?;
    try testing.expectEqualSlices(u8, asked.name.wire(), echoed.name.wire());
    try testing.expect(packet.findOptRecord(p) == null);
    try testing.expectEqual(@as(u16, 0), p.header.arcount);

    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "an OPT record in the authority section gets FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    const p_query = try packet.parse(query_with_opt_in_authority);
    try testing.expect(packet.findOptRecord(p_query) == null);

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_with_opt_in_authority, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(@as(u16, 1), p.header.qdcount);
    try testing.expect(packet.findOptRecord(p) == null);

    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "two OPT records in the additional section get FORMERR" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    // Here the rejection comes from `packet.parse`, one section walk earlier.
    try testing.expectError(error.MultipleOptRecords, packet.parse(query_with_two_opts));

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_with_two_opts, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expect(packet.findOptRecord(p) == null);

    try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

test "a query with a valid OPT record is still forwarded" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var query_buf: [query_with_opt_len]u8 = undefined;
    const query = queryWithOpt(&query_buf, 1232, true);

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query, &buf));

    try testing.expectEqualSlices(u8, response_bytes, reply);
    try testing.expectEqual(@as(usize, 1), fake.calls);
    try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.formerr.load(.monotonic));

    // The same query, answered from the SERVFAIL path, still echoes the OPT.
    var fail: FakeUpstream = .{ .err = error.Timeout };
    var h2_owner: upstream_owner.Borrowed = .{};
    var h2 = bare(h2_owner.client(fail.client()));
    const synthesized = try expectReply(udp(&h2, t.io(), query, &buf));
    const p = try packet.parse(synthesized);
    const opt = try edns.parseOpt(synthesized, packet.findOptRecord(p).?);
    try testing.expectEqual(true, opt.do_bit);
    try testing.expectEqual(@as(u16, 1232), opt.udp_payload_size);
    try testing.expectEqual(@as(u64, 0), h2.stats.formerr.load(.monotonic));
}

test "every synthesized reply re-parses and binds to its request" {
    var t: TestIo = .init();
    defer t.deinit();

    const Case = struct {
        query: []const u8,
        err: ?transport.ExchangeError,
        rcode: types.Rcode,
    };
    const broken_question = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
        "\xc0\x0c\x00\x01\x00\x01";
    const no_question = "\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00";
    const update = "\x12\x34\x29\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
        "\x07example\x03com\x00\x00\x01\x00\x01";

    const cases = [_]Case{
        .{ .query = broken_question, .err = null, .rcode = .form_err },
        .{ .query = no_question, .err = null, .rcode = .form_err },
        .{ .query = query_with_named_opt, .err = null, .rcode = .form_err },
        .{ .query = query_with_bad_option, .err = null, .rcode = .form_err },
        .{ .query = query_with_opt_in_answer, .err = null, .rcode = .form_err },
        .{ .query = query_with_opt_in_authority, .err = null, .rcode = .form_err },
        .{ .query = query_with_two_opts, .err = null, .rcode = .form_err },
        .{ .query = update, .err = null, .rcode = .not_imp },
        .{ .query = query_bytes, .err = error.BadResponse, .rcode = .serv_fail },
        .{ .query = query_bytes, .err = error.Unexpected, .rcode = .serv_fail },
    };

    for (cases) |c| {
        var fake: FakeUpstream = .{ .reply = response_bytes, .err = c.err };
        var h_owner: upstream_owner.Borrowed = .{};
        var h = bare(h_owner.client(fake.client()));

        var buf: [udp_limit_min]u8 = undefined;
        const reply = try expectReply(udp(&h, t.io(), c.query, &buf));

        const p = try packet.parse(reply);
        try testing.expectEqual(c.rcode, p.header.flags.rcode);
        try testing.expectEqual(@as(u16, 0x1234), p.header.id);
        try testing.expectEqual(true, p.header.flags.qr);
        try testing.expectEqual(true, p.header.flags.ra);
        try testing.expectEqual(@as(u16, 0), p.header.ancount);
        try testing.expectEqual(@as(u16, 0), p.header.nscount);
    }
}

// --- pipeline fixtures -----------------------------------------------------

const Allocator = std.mem.Allocator;

/// A query with an arbitrary name, type and class, and no OPT record.
fn queryFor(
    buf: []u8,
    id: u16,
    domain: []const u8,
    qtype: types.Type,
    qclass: types.Class,
) []const u8 {
    var w: std.Io.Writer = .fixed(buf);
    var encoded: [types.header_len]u8 = undefined;
    header.encode(.{
        .id = id,
        .flags = .{
            .rcode = .no_error,
            .z = 0,
            .ra = false,
            .rd = true,
            .tc = false,
            .aa = false,
            .opcode = .query,
            .qr = false,
        },
        .qdcount = 1,
        .ancount = 0,
        .nscount = 0,
        .arcount = 0,
    }, &encoded);
    w.writeAll(&encoded) catch unreachable;
    question.encode(.{
        .name = name.fromText(domain) catch unreachable,
        .qtype = qtype,
        .qclass = qclass,
    }, &w) catch unreachable;
    return w.buffered();
}

/// The same query with an OPT record carrying `options_bytes` verbatim.
fn queryWithOptions(buf: []u8, domain: []const u8, options_bytes: []const u8) []const u8 {
    var w: std.Io.Writer = .fixed(buf);
    var encoded: [types.header_len]u8 = undefined;
    header.encode(.{
        .id = 0x1234,
        .flags = .{
            .rcode = .no_error,
            .z = 0,
            .ra = false,
            .rd = true,
            .tc = false,
            .aa = false,
            .opcode = .query,
            .qr = false,
        },
        .qdcount = 1,
        .ancount = 0,
        .nscount = 0,
        .arcount = 1,
    }, &encoded);
    w.writeAll(&encoded) catch unreachable;
    question.encode(.{
        .name = name.fromText(domain) catch unreachable,
        .qtype = .a,
        .qclass = .in,
    }, &w) catch unreachable;
    edns.encodeOpt(.{
        .udp_payload_size = 1232,
        .extended_rcode = 0,
        .version = 0,
        .do_bit = false,
        .options = .{ .offset = 0, .len = 0 },
    }, options_bytes, &w) catch unreachable;
    return w.buffered();
}

/// An EDNS Client Subnet option for `a.b.c.0/24`.
fn ecsOption(buf: *[11]u8, third: u8) []const u8 {
    const bytes = [_]u8{
        0x00, 0x08, // code 8
        0x00, 0x07, // length
        0x00, 0x01, // family: IPv4
        24,    0, // source prefix, scope prefix
        192,   168,
        third,
    };
    @memcpy(buf, &bytes);
    return buf;
}

/// A DNS cookie option (code 10), which a strip must leave alone.
const cookie_option = "\x00\x0a\x00\x08\x01\x02\x03\x04\x05\x06\x07\x08";

/// A response to `query` whose answer section is the CNAME chain
/// `names[0] → names[1] → …`. `names[0]` is the queried name.
fn cnameChain(buf: []u8, query: []const u8, names: []const []const u8) []u8 {
    const p = packet.parse(query) catch unreachable;
    const q = packet.firstQuestion(p).?;
    var b = packet.ResponseBuilder.init(buf, p.header, q) catch unreachable;
    var i: usize = 0;
    while (i + 1 < names.len) : (i += 1) {
        const owner = name.fromText(names[i]) catch unreachable;
        const target = name.fromText(names[i + 1]) catch unreachable;
        b.addAnswer(owner, .cname, .in, 300, target.wire()) catch unreachable;
    }
    return b.finish();
}

const SnapshotFixture = struct {
    groups: []const model.Group = &.{.{ .name = "default" }},
    group_ids: []const i64 = &.{1},
    rules: []const model.Rule = &.{},
    sources: []const model.BlocklistSource = &.{},
    source_ids: []const i64 = &.{},
    group_sources: []const model.GroupSource = &.{},
    compiled: []const ?matcher.Snapshot.Compiled = &.{},
};

fn buildSnapshot(gpa: Allocator, fixture: SnapshotFixture) !matcher.Snapshot {
    return matcher.Snapshot.build(gpa, .{
        .groups = fixture.groups,
        .group_ids = fixture.group_ids,
        .group_sources = fixture.group_sources,
        .sources = fixture.sources,
        .source_ids = fixture.source_ids,
        .rules = fixture.rules,
        .clients = &.{},
        .prefixes = &.{},
        .compiled = fixture.compiled,
        .seed = 0x5eed,
        .generation = 1,
    });
}

/// `Manager.acquire` reads the manager's lock and its current snapshot and
/// nothing else, so a manager that publishes one hand-built snapshot needs
/// none of the database, fetcher or blocklist directory the real one owns.
fn fixtureManager(m: *manager.Manager, snapshot: *matcher.Snapshot) void {
    m.* = .{
        .gpa = testing.allocator,
        .database = undefined,
        .paths = undefined,
        .fetcher = undefined,
        .update = .{},
        .schedule_mutex = .init,
        .schedule_version = 0,
        .schedule_anchor_s = null,
        .schedule_event = .unset,
        .schedule_clock = .real,
        .total_budget = forward_timeout,
        .lock = .init,
        .writer_lock = .init,
        .refresh_lock = .init,
        .current = snapshot,
        .generation = 1,
        .statuses = &.{},
        .status_arena = .init(testing.allocator),
    };
}

fn blockRule(pattern: []const u8) model.Rule {
    return .{ .group = "default", .pattern = pattern, .kind = .exact, .action = .block };
}

fn allowRule(pattern: []const u8) model.Rule {
    return .{ .group = "default", .pattern = pattern, .kind = .exact, .action = .allow };
}

/// The A record of the first answer, which the blocking mode `.zero` sets to
/// 0.0.0.0.
fn firstAnswer(p: packet.Packet) !record.Record {
    var it = packet.answers(p);
    return (try it.next()) orelse error.TestExpectedAnswer;
}

fn drainLog(lg: *logger_mod.Logger, io: std.Io, out: []logger_mod.Entry) []logger_mod.Entry {
    const n = lg.queue.getUncancelable(io, out, 0) catch 0;
    return out[0..n];
}

// --- policy snapshots ------------------------------------------------------

/// Parks one query inside its upstream exchange so that a `setPolicy` lands in
/// the middle of it. `ecs_mode` is read before the exchange and `blocking`
/// after it, on the uncloak path, so the two together are what a torn query
/// would expose.
const ParkedUpstream = struct {
    reply: []const u8,
    /// Set once the exchange has the query in hand.
    reached: std.Io.Event = .unset,
    /// Set by the test to let the exchange answer.
    release: std.Io.Event = .unset,
    seen: [1024]u8 = undefined,
    seen_len: usize = 0,

    fn exchangeFn(
        ptr: *anyopaque,
        io: std.Io,
        query: []const u8,
        response_buf: []u8,
        selected: *?[]const u8,
    ) transport.ExchangeError![]u8 {
        const self: *ParkedUpstream = @ptrCast(@alignCast(ptr));
        selected.* = "fake://parked-upstream";
        self.seen_len = @min(query.len, self.seen.len);
        @memcpy(self.seen[0..self.seen_len], query[0..self.seen_len]);

        self.reached.set(io);
        self.release.wait(io) catch {};

        if (self.reply.len > response_buf.len) return error.ResponseTooLarge;
        @memcpy(response_buf[0..self.reply.len], self.reply);
        const bytes = response_buf[0..self.reply.len];
        packet.setId(bytes, (header.parse(query) catch unreachable).id);
        return bytes;
    }

    fn client(self: *ParkedUpstream) transport.Client {
        return .{ .ptr = self, .exchangeFn = exchangeFn };
    }

    fn sent(self: *const ParkedUpstream) []const u8 {
        return self.seen[0..self.seen_len];
    }
};

/// One query on its own task, reduced to the two facts the interleaving test
/// asserts on. Nothing is asserted here: a failing assertion on a task the test
/// has yet to await would leave `Threaded.deinit` joining a task nothing ends.
const ParkedQuery = struct {
    handler: *Handler,
    io: std.Io,
    query: []const u8,
    buf: [udp_limit_min]u8 = undefined,
    answer_ttl: u32 = 0,
    failed: bool = false,

    fn run(self: *ParkedQuery) void {
        const bytes = switch (udp(self.handler, self.io, self.query, &self.buf)) {
            .reply => |b| b,
            .drop => {
                self.failed = true;
                return;
            },
        };
        const p = packet.parse(bytes) catch {
            self.failed = true;
            return;
        };
        const answer = firstAnswer(p) catch {
            self.failed = true;
            return;
        };
        self.answer_ttl = answer.ttl;
    }
};

fn ecsPresent(message: []const u8) !bool {
    const p = try packet.parse(message);
    const rec = packet.findOptRecord(p) orelse return false;
    const opt = try edns.parseOpt(message, rec);
    return (try edns.findOption(message, opt, edns.ecs_option_code)) != null;
}

test "a setPolicy in the middle of a query changes nothing that query reads" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .rules = &.{blockRule("bad.example.org")},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var ecs_buf: [11]u8 = undefined;
    var query_buf: [512]u8 = undefined;
    const query = queryWithOptions(&query_buf, "example.com", ecsOption(&ecs_buf, 1));

    var chain_buf: [512]u8 = undefined;
    const chain = cnameChain(&chain_buf, query, &.{ "example.com", "bad.example.org" });

    var up: ParkedUpstream = .{ .reply = chain };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(up.client()));
    h.manager = &mgr;
    const before: Policy = .{
        .blocking = .{ .mode = .zero, .ttl = 5 },
        .ecs_mode = .strip,
        .forward_read_timeout = forward_timeout,
    };
    const after: Policy = .{
        .blocking = .{ .mode = .zero, .ttl = 999 },
        .ecs_mode = .forward,
        .forward_read_timeout = forward_timeout,
    };
    h.setPolicy(t.io(), before);

    var parked: ParkedQuery = .{ .handler = &h, .io = t.io(), .query = query };
    var future = try t.io().concurrent(ParkedQuery.run, .{&parked});
    try up.reached.wait(t.io());
    h.setPolicy(t.io(), after);
    up.release.set(t.io());
    future.await(t.io());

    try testing.expect(!parked.failed);
    // Read before the exchange, under `before`: the subnet was stripped.
    try testing.expect(!try ecsPresent(up.sent()));
    // Read after the exchange, on the uncloak path, still under `before`.
    try testing.expectEqual(@as(u32, 5), parked.answer_ttl);

    // The publish did take effect — for the next query, which is the whole
    // point: the snapshot is per query, not a way to ignore a change.
    var next: ParkedQuery = .{ .handler = &h, .io = t.io(), .query = query };
    next.run();
    try testing.expect(!next.failed);
    try testing.expect(try ecsPresent(up.sent()));
    try testing.expectEqual(@as(u32, 999), next.answer_ttl);
}

// --- rate limit ------------------------------------------------------------

test "a client over its rate limit gets REFUSED before the query is parsed" {
    var t: TestIo = .init();
    defer t.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var limiter: rate_limiter.RateLimiter = try .init(
        testing.allocator,
        .{ .limit = 2, .window_seconds = 60 },
    );
    defer limiter.deinit();
    h.limiter = &limiter;

    var buf: [udp_limit_min]u8 = undefined;
    for (0..2) |_| {
        const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));
        try testing.expectEqual(types.Rcode.no_error, (try packet.parse(reply)).header.flags.rcode);
    }

    const refused = try expectReply(udp(&h, t.io(), query_bytes, &buf));
    const p = try packet.parse(refused);
    try testing.expectEqual(types.Rcode.refused, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(@as(u64, 1), h.stats.refused.load(.monotonic));
    try testing.expectEqual(@as(usize, 2), fake.calls);

    // A different client has its own window.
    const other: address.NetAddress = .{ .ip4 = .{ 192, 168, 1, 51 } };
    _ = try expectReply(handleQuery(&h, t.io(), .udp, other, query_bytes, &buf));
    try testing.expectEqual(@as(usize, 3), fake.calls);
}

// --- local records ---------------------------------------------------------

test "a local record answers authoritatively without reaching the upstream" {
    var t: TestIo = .init();
    defer t.deinit();

    var table = try records.Records.build(testing.allocator, &.{
        .{ .name = "nas.lan", .rtype = .a, .value = "192.168.1.10", .ttl = 60 },
    });
    defer table.deinit(testing.allocator);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    var tables: local_tables_mod.LocalTables = .{ .records = table };
    h.local_tables = &tables;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, "nas.lan", .a, .in);

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query, &buf));

    const p = try packet.parse(reply);
    try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
    try testing.expectEqual(true, p.header.flags.aa);
    try testing.expectEqual(@as(u16, 1), p.header.ancount);

    const answer = try firstAnswer(p);
    try testing.expectEqual(types.Type.a, answer.rtype);
    try testing.expectEqual(@as(u32, 60), answer.ttl);
    try testing.expectEqual([4]u8{ 192, 168, 1, 10 }, try record.rdataA(p.bytes, answer));

    try testing.expectEqual(@as(usize, 0), fake.calls);
    try testing.expectEqual(@as(u64, 1), h.stats.local_answers.load(.monotonic));
}

test "a local name with no record of the queried type is authoritative NODATA" {
    var t: TestIo = .init();
    defer t.deinit();

    var table = try records.Records.build(testing.allocator, &.{
        .{ .name = "nas.lan", .rtype = .a, .value = "192.168.1.10", .ttl = 60 },
    });
    defer table.deinit(testing.allocator);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    var tables: local_tables_mod.LocalTables = .{ .records = table };
    h.local_tables = &tables;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, "nas.lan", .aaaa, .in);

    var buf: [udp_limit_min]u8 = undefined;
    const p = try packet.parse(try expectReply(udp(&h, t.io(), query, &buf)));

    try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
    try testing.expectEqual(true, p.header.flags.aa);
    try testing.expectEqual(@as(u16, 0), p.header.ancount);
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

// --- forward zones ---------------------------------------------------------

test "a forward zone answers from the cache and never reaches the pool" {
    var t: TestIo = .init();
    defer t.deinit();

    var zones = try forward_zones.Zones.build(testing.allocator, &.{
        .{ .zone = "lan.home", .resolver = "udp://127.0.0.1:1" },
    });
    defer zones.deinit(testing.allocator);

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    // The name is blocklisted, so only the zone path can answer it from the
    // cache: the upstream path would have blocked it before the lookup.
    var snapshot = try buildSnapshot(testing.allocator, .{ .rules = &.{blockRule("nas.lan.home")} });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    var tables: local_tables_mod.LocalTables = .{ .zones = zones };
    h.local_tables = &tables;
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;
    h.manager = &mgr;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, "nas.lan.home", .a, .in);

    // The zone resolver is unreachable, so the answer can only come from the
    // entry planted under the key the handler builds.
    var zone_reply_buf: [512]u8 = undefined;
    const zone_p = try packet.parse(query);
    var b = try packet.ResponseBuilder.init(&zone_reply_buf, zone_p.header, packet.firstQuestion(zone_p).?);
    try b.addAnswer(try name.fromText("nas.lan.home"), .a, .in, 300, "\xc0\xa8\x01\x0a");
    const zone_reply = b.finish();

    var key_buf: [dns_cache.max_key_len]u8 = undefined;
    const key = dns_cache.buildKey(
        &key_buf,
        "nas.lan.home",
        @intFromEnum(types.Type.a),
        @intFromEnum(types.Class.in),
        false,
        null,
    );
    try cache.put(std.Io.Clock.real.now(t.io()).toSeconds(), key, zone_reply, .{ .ttl_seconds = 300, .negative = false });

    var buf: [udp_limit_min]u8 = undefined;
    const p = try packet.parse(try expectReply(udp(&h, t.io(), query, &buf)));

    try testing.expectEqual(@as(u16, 1), p.header.ancount);
    try testing.expectEqual([4]u8{ 192, 168, 1, 10 }, try record.rdataA(p.bytes, try firstAnswer(p)));
    try testing.expectEqual(@as(usize, 0), fake.calls);
    try testing.expectEqual(@as(u64, 1), h.stats.cache_hits.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.blocked.load(.monotonic));
}

test "a forward zone bypasses the blocklist and fails on its own resolver" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .rules = &.{blockRule("ads.lan.home")},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var zones = try forward_zones.Zones.build(testing.allocator, &.{
        .{ .zone = "lan.home", .resolver = "udp://127.0.0.1:1" },
    });
    defer zones.deinit(testing.allocator);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    var tables: local_tables_mod.LocalTables = .{ .zones = zones };
    h.local_tables = &tables;
    h.manager = &mgr;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, "ads.lan.home", .a, .in);

    var buf: [udp_limit_min]u8 = undefined;
    const p = try packet.parse(try expectReply(udp(&h, t.io(), query, &buf)));

    // A blocked answer would be NOERROR with an address, so SERVFAIL is proof
    // the query went to the zone resolver instead of the blocklist.
    try testing.expectEqual(types.Rcode.serv_fail, p.header.flags.rcode);
    try testing.expectEqual(@as(u64, 0), h.stats.blocked.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);
}

// --- filtering -------------------------------------------------------------

test "a blocked domain gets the zero address with the blocking ttl" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{ .rules = &.{blockRule("example.com")} });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;

    var buf: [udp_limit_min]u8 = undefined;
    const p = try packet.parse(try expectReply(udp(&h, t.io(), query_bytes, &buf)));

    try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 1), p.header.ancount);

    const answer = try firstAnswer(p);
    try testing.expectEqual(@as(u32, blocking.ttl), answer.ttl);
    try testing.expectEqual([4]u8{ 0, 0, 0, 0 }, try record.rdataA(p.bytes, answer));

    try testing.expectEqual(@as(usize, 0), fake.calls);
    try testing.expectEqual(@as(u64, 1), h.stats.blocked.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.unfiltered_queries.load(.monotonic));
}

test "an allow rule beats the block rule and the query reaches the upstream" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .rules = &.{ blockRule("com"), allowRule("example.com") },
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    try testing.expectEqualSlices(u8, response_bytes, reply);
    try testing.expectEqual(@as(usize, 1), fake.calls);
    try testing.expectEqual(@as(u64, 0), h.stats.blocked.load(.monotonic));
}

test "a handler with no snapshot answers unfiltered and counts it" {
    var t: TestIo = .init();
    defer t.deinit();

    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, undefined);
    mgr.current = null;

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;

    var buf: [udp_limit_min]u8 = undefined;
    const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    try testing.expectEqualSlices(u8, response_bytes, reply);
    try testing.expectEqual(@as(u64, 1), h.stats.unfiltered_queries.load(.monotonic));
}

// --- cache -----------------------------------------------------------------

test "a miss stores the answer and the next query is a hit with a fresh id" {
    var t: TestIo = .init();
    defer t.deinit();

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query_bytes, &buf));
    try testing.expectEqual(@as(u32, 1), cache.len());
    try testing.expectEqual(@as(usize, 1), fake.calls);

    var query_buf: [512]u8 = undefined;
    const again = queryFor(&query_buf, 0x5678, "example.com", .a, .in);
    const p = try packet.parse(try expectReply(udp(&h, t.io(), again, &buf)));

    try testing.expectEqual(@as(u16, 0x5678), p.header.id);
    try testing.expectEqual(@as(u16, 1), p.header.ancount);
    try testing.expectEqual(@as(usize, 1), fake.calls);
    try testing.expectEqual(@as(u64, 1), h.stats.cache_hits.load(.monotonic));
}

test "a cache and a rate-limiter swap land in the middle of a query without a dangling read" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var retired_cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    var live_cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer live_cache.deinit();

    var retired_limiter: rate_limiter.RateLimiter = try .init(
        testing.allocator,
        .{ .limit = 100, .window_seconds = 60 },
    );
    var live_limiter: rate_limiter.RateLimiter = try .init(
        testing.allocator,
        .{ .limit = 100, .window_seconds = 60 },
    );
    defer live_limiter.deinit();

    var up: ParkedUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(up.client()));
    h.cache = &retired_cache;
    h.limiter = &retired_limiter;
    h.policy.negative_ttl_max = 3600;

    // The parked query is past its limiter charge and its cache lookup, and
    // has not reached its cache store: both swaps land while it is inside the
    // exchange, which is where a pointer loaded before the lock would leave
    // the query writing into freed memory.
    var parked: ParkedQuery = .{ .handler = &h, .io = io, .query = query_bytes };
    var future = try io.concurrent(ParkedQuery.run, .{&parked});
    try up.reached.wait(io);

    try testing.expectEqual(&retired_cache, h.replaceCache(io, &live_cache).?);
    try testing.expectEqual(&retired_limiter, h.replaceRateLimiter(io, &live_limiter).?);
    // Retire, which is the caller's: `replace` only publishes.
    retired_cache.deinit();
    retired_limiter.deinit();

    up.release.set(io);
    future.await(io);
    try testing.expect(!parked.failed);

    // The store went to the generation that is live now, and the next query
    // reads it there.
    try testing.expectEqual(@as(u32, 1), live_cache.len());
    var buf: [udp_limit_min]u8 = undefined;
    var query_buf: [512]u8 = undefined;
    const again = queryFor(&query_buf, 0x5678, "example.com", .a, .in);
    _ = try expectReply(udp(&h, io, again, &buf));
    try testing.expectEqual(@as(u64, 1), h.stats.cache_hits.load(.monotonic));

    // A handler can also be left with neither, which is what a caller that
    // turns the cache off publishes.
    try testing.expectEqual(&live_cache, h.replaceCache(io, null).?);
    try testing.expectEqual(&live_limiter, h.replaceRateLimiter(io, null).?);
    _ = try expectReply(udp(&h, io, again, &buf));
}

test "a blocked answer is never cached" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{ .rules = &.{blockRule("example.com")} });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    try testing.expectEqual(@as(u32, 0), cache.len());
    try testing.expectEqual(@as(u64, 1), h.stats.blocked.load(.monotonic));
}

// --- CNAME uncloaking ------------------------------------------------------

test "a CNAME chain into a blocked target blocks the original question" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .rules = &.{blockRule("bad.example.org")},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var chain_buf: [512]u8 = undefined;
    const chain = cnameChain(&chain_buf, query_bytes, &.{ "example.com", "bad.example.org" });

    var fake: FakeUpstream = .{ .reply = chain };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;

    var buf: [udp_limit_min]u8 = undefined;
    const p = try packet.parse(try expectReply(udp(&h, t.io(), query_bytes, &buf)));

    try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
    try testing.expectEqual(@as(u16, 1), p.header.ancount);

    // The answer is about the name the client asked for, not the target.
    const answer = try firstAnswer(p);
    try testing.expectEqual(types.Type.a, answer.rtype);
    try testing.expectEqualSlices(
        u8,
        (try name.fromText("example.com")).wire(),
        answer.name.wire(),
    );
    try testing.expectEqual([4]u8{ 0, 0, 0, 0 }, try record.rdataA(p.bytes, answer));
    try testing.expectEqual(@as(u64, 1), h.stats.uncloak_blocked.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.blocked.load(.monotonic));
}

test "uncloaking follows eight links and stops" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .rules = &.{blockRule("bad.example.org")},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    const hops = [_][]const u8{
        "h1.example.net", "h2.example.net", "h3.example.net", "h4.example.net",
        "h5.example.net", "h6.example.net", "h7.example.net", "h8.example.net",
    };

    // `links` CNAME records, the last of which points at the blocked name.
    for ([_]usize{ 8, 9 }) |links| {
        var names: [11][]const u8 = undefined;
        names[0] = "example.com";
        for (0..links - 1) |i| names[i + 1] = hops[i];
        names[links] = "bad.example.org";

        var chain_buf: [1024]u8 = undefined;
        const chain = cnameChain(&chain_buf, query_bytes, names[0 .. links + 1]);

        var fake: FakeUpstream = .{ .reply = chain };
        var h_owner: upstream_owner.Borrowed = .{};
        var h = bare(h_owner.client(fake.client()));
        h.manager = &mgr;

        var buf: [udp_limit_min]u8 = undefined;
        const reply = try expectReply(udp(&h, t.io(), query_bytes, &buf));

        if (links == max_cname_depth) {
            try testing.expectEqual(@as(u64, 1), h.stats.uncloak_blocked.load(.monotonic));
        } else {
            try testing.expectEqual(@as(u64, 0), h.stats.uncloak_blocked.load(.monotonic));
            try testing.expectEqualSlices(u8, chain, reply);
        }
    }
}

// --- safe search -----------------------------------------------------------

test "safe search sends the target upstream and answers the original question" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .groups = &.{.{ .name = "default", .safe_search = true }},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    // The upstream answers the rewritten question with one address and one
    // record of another type, which must not be copied over.
    var target_buf: [512]u8 = undefined;
    const target_query = queryFor(&target_buf, 0x1234, "forcesafesearch.google.com", .a, .in);
    var upstream_buf: [512]u8 = undefined;
    const target_p = try packet.parse(target_query);
    var b = try packet.ResponseBuilder.init(
        &upstream_buf,
        target_p.header,
        packet.firstQuestion(target_p).?,
    );
    const target_name = try name.fromText("forcesafesearch.google.com");
    try b.addAnswer(target_name, .a, .in, 120, "\x08\x08\x08\x08");
    try b.addAnswer(target_name, .txt, .in, 60, "\x03abc");

    var fake: FakeUpstream = .{ .reply = b.finish() };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, "www.google.com", .a, .in);

    var buf: [udp_limit_min]u8 = undefined;
    const p = try packet.parse(try expectReply(udp(&h, t.io(), query, &buf)));

    // What went out carried the target name.
    const sent = try packet.parse(fake.sent());
    try testing.expectEqualSlices(
        u8,
        target_name.wire(),
        packet.firstQuestion(sent).?.name.wire(),
    );

    // What came back keeps the original question.
    try testing.expectEqualSlices(
        u8,
        (try name.fromText("www.google.com")).wire(),
        packet.firstQuestion(p).?.name.wire(),
    );
    try testing.expectEqual(@as(u16, 2), p.header.ancount);

    var it = packet.answers(p);
    const cname = (try it.next()).?;
    try testing.expectEqual(types.Type.cname, cname.rtype);
    try testing.expectEqual(@as(u32, 120), cname.ttl);
    try testing.expectEqualSlices(u8, target_name.wire(), (try record.rdataCname(p.bytes, cname)).wire());

    const a = (try it.next()).?;
    try testing.expectEqual(types.Type.a, a.rtype);
    try testing.expectEqualSlices(u8, target_name.wire(), a.name.wire());
    try testing.expectEqual([4]u8{ 8, 8, 8, 8 }, try record.rdataA(p.bytes, a));
    try testing.expect((try it.next()) == null);

    try testing.expectEqual(@as(u64, 1), h.stats.safesearch_rewrites.load(.monotonic));
}

// --- ECS -------------------------------------------------------------------

test "ecs strip removes option 8 from the outgoing query and keeps the rest" {
    var t: TestIo = .init();
    defer t.deinit();

    var ecs_buf: [11]u8 = undefined;
    var options_buf: [64]u8 = undefined;
    const ecs = ecsOption(&ecs_buf, 1);
    @memcpy(options_buf[0..cookie_option.len], cookie_option);
    @memcpy(options_buf[cookie_option.len..][0..ecs.len], ecs);
    const options_bytes = options_buf[0 .. cookie_option.len + ecs.len];

    var query_buf: [512]u8 = undefined;
    const query = queryWithOptions(&query_buf, "example.com", options_bytes);

    // The query really carries a subnet, so the assertions below measure a
    // strip rather than an absence.
    const asked = try packet.parse(query);
    const asked_opt = try edns.parseOpt(query, packet.findOptRecord(asked).?);
    try testing.expect((try edns.findOption(query, asked_opt, edns.ecs_option_code)) != null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query, &buf));

    const sent = try packet.parse(fake.sent());
    const opt = try edns.parseOpt(fake.sent(), packet.findOptRecord(sent).?);
    try testing.expect((try edns.findOption(fake.sent(), opt, edns.ecs_option_code)) == null);

    const cookie = (try edns.findOption(fake.sent(), opt, 10)).?;
    try testing.expectEqualSlices(u8, cookie_option[4..], cookie.data);
    try testing.expectEqual(@as(u64, 0), h.stats.ecs_strip_failed.load(.monotonic));
}

test "ecs forward keeps the subnet in the query and splits the cache key" {
    var t: TestIo = .init();
    defer t.deinit();

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.policy.ecs_mode = .forward;
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;

    var buf: [udp_limit_min]u8 = undefined;
    for ([_]u8{ 1, 2 }) |third| {
        var ecs_buf: [11]u8 = undefined;
        var query_buf: [512]u8 = undefined;
        const query = queryWithOptions(&query_buf, "example.com", ecsOption(&ecs_buf, third));
        _ = try expectReply(udp(&h, t.io(), query, &buf));

        // `.forward` sends the query unchanged, subnet included.
        const sent = try packet.parse(fake.sent());
        const opt = try edns.parseOpt(fake.sent(), packet.findOptRecord(sent).?);
        try testing.expect((try edns.findOption(fake.sent(), opt, edns.ecs_option_code)) != null);
    }

    // Two subnets, two entries, two upstream exchanges.
    try testing.expectEqual(@as(u32, 2), cache.len());
    try testing.expectEqual(@as(usize, 2), fake.calls);

    var ecs_buf: [11]u8 = undefined;
    var query_buf: [512]u8 = undefined;
    const repeat = queryWithOptions(&query_buf, "example.com", ecsOption(&ecs_buf, 1));
    _ = try expectReply(udp(&h, t.io(), repeat, &buf));
    try testing.expectEqual(@as(usize, 2), fake.calls);
    try testing.expectEqual(@as(u64, 1), h.stats.cache_hits.load(.monotonic));
}

// --- pause, class and tracking ---------------------------------------------

test "a paused handler answers a blocked domain from the upstream" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{ .rules = &.{blockRule("example.com")} });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var paused: pause.Pause = .{};
    paused.pauseFor(0, null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.pause = &paused;

    var buf: [udp_limit_min]u8 = undefined;
    try testing.expectEqualSlices(u8, response_bytes, try expectReply(udp(&h, t.io(), query_bytes, &buf)));
    try testing.expectEqual(@as(u64, 1), h.stats.paused_queries.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.blocked.load(.monotonic));

    // Resuming puts the block back without a restart.
    paused.unpause();
    const p = try packet.parse(try expectReply(udp(&h, t.io(), query_bytes, &buf)));
    try testing.expectEqual([4]u8{ 0, 0, 0, 0 }, try record.rdataA(p.bytes, try firstAnswer(p)));
    try testing.expectEqual(@as(u64, 1), h.stats.blocked.load(.monotonic));
}

test "a non-IN class query skips filtering and the cache" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{ .rules = &.{blockRule("example.com")} });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, "example.com", .txt, .ch);

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query, &buf));

    try testing.expectEqual(@as(usize, 1), fake.calls);
    try testing.expectEqual(@as(u64, 0), h.stats.blocked.load(.monotonic));
    try testing.expectEqual(@as(u32, 0), cache.len());
}

test "the tracker sees the client address of every parsed query" {
    var t: TestIo = .init();
    defer t.deinit();

    var tracker_days: retention_mod.RetentionDays = .init(30);
    var tracker: clients.Tracker = .init(&tracker_days);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.tracker = &tracker;

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query_bytes, &buf));
    _ = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    try testing.expectEqual(@as(u32, 1), tracker.pendingClients(t.io()));
    try testing.expectEqual(@as(u64, 2), tracker.snapshotStats(t.io()).tracked);
    try testing.expectEqual(@as(u64, 0), h.stats.tracker_full.load(.monotonic));
}

// --- query log -------------------------------------------------------------

/// One expected `query_log` row. Every provenance field is asserted on every
/// path, so a value that leaks from one path into another fails here rather
/// than surviving as a plausible-looking row.
///
/// The defaults are what a row says when nothing set the field: no group (no
/// snapshot), no rule matched, no source, no rewrite, no upstream attempted.
const ExpectedRow = struct {
    domain: []const u8,
    qtype: types.Type = .a,
    qclass: types.Class = .in,
    rcode: u12 = @intFromEnum(types.Rcode.no_error),
    blocked: bool = false,
    cache_hit: ?bool = null,
    upstream: []const u8 = "",
    group_id: ?i64 = null,
    group_name: []const u8 = "",
    action: provenance.PolicyAction,
    reason: provenance.PolicyReason,
    matched: []const u8 = "",
    source_id: ?i64 = null,
    source_name: []const u8 = "",
    cname_target: []const u8 = "",
    safe_search_target: []const u8 = "",
    route: provenance.RouteKind,
    forward_zone: []const u8 = "",
};

fn expectRow(expected: ExpectedRow, entry: *const logger_mod.Entry) !void {
    try testing.expectEqualStrings(expected.domain, entry.domain());
    try testing.expectEqualStrings("192.168.1.50", entry.clientIp());
    try testing.expectEqual(@as(?u16, @intFromEnum(expected.qtype)), entry.qtype);
    try testing.expectEqual(@as(u16, @intFromEnum(expected.qclass)), entry.qclass);
    try testing.expectEqual(@as(u16, expected.rcode), entry.rcode);
    try testing.expectEqual(expected.blocked, entry.blocked);
    try testing.expectEqual(expected.cache_hit, entry.cache_hit);
    try testing.expectEqualStrings(expected.upstream, entry.upstream());
    try testing.expectEqual(expected.group_id, entry.group_id);
    try testing.expectEqualStrings(expected.group_name, entry.groupName());
    try testing.expectEqual(expected.action, entry.policy_action);
    try testing.expectEqual(expected.reason, entry.policy_reason);
    try testing.expectEqualStrings(expected.matched, entry.matched());
    try testing.expectEqual(expected.source_id, entry.source_id);
    try testing.expectEqualStrings(expected.source_name, entry.sourceName());
    try testing.expectEqualStrings(expected.cname_target, entry.cnameTarget());
    try testing.expectEqualStrings(expected.safe_search_target, entry.safeSearchTarget());
    try testing.expectEqual(expected.route, entry.route_kind);
    try testing.expectEqualStrings(expected.forward_zone, entry.forwardZone());
    try testing.expect(entry.response_time_us.? >= 0);
}

/// The one blocklist source the provenance tests filter against: one blocked
/// name and one `@@` exception, so a row can name the list that decided either
/// way.
const provenance_sources = [_]model.BlocklistSource{
    .{ .url = "https://lists.test/a", .name = "list a" },
};
const provenance_source_ids = [_]i64{11};
const provenance_links = [_]model.GroupSource{
    .{ .group = "default", .source_url = "https://lists.test/a" },
};
const provenance_compiled = [_]?matcher.Snapshot.Compiled{.{
    .list_body = "ads.example.net\n",
    .wild_body = "",
    .allow_body = "safe.example.net\n",
}};

fn provenanceSnapshot(rules: []const model.Rule) !matcher.Snapshot {
    return buildSnapshot(testing.allocator, .{
        .rules = rules,
        .sources = &provenance_sources,
        .source_ids = &provenance_source_ids,
        .group_sources = &provenance_links,
        .compiled = &provenance_compiled,
    });
}

test "the upstream pipeline explains every allowed and blocked answer" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var snapshot = try provenanceSnapshot(&.{
        blockRule("blocked.example.com"),
        allowRule("allowed.example.com"),
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    var queue_buf: [8]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;
    h.sink = &sink;

    const names = [_][]const u8{
        "example.com",
        "example.com",
        "allowed.example.com",
        "blocked.example.com",
        "ads.example.net",
        "safe.example.net",
    };
    var buf: [udp_limit_min]u8 = undefined;
    for (names, 0..) |domain, i| {
        var query_buf: [512]u8 = undefined;
        const query = queryFor(&query_buf, @intCast(i + 1), domain, .a, .in);
        _ = try expectReply(udp(&h, io, query, &buf));
    }

    const expected = [_]ExpectedRow{
        .{
            .domain = "example.com",
            .group_id = 1,
            .group_name = "default",
            .action = .allow,
            .reason = .no_match,
            .route = .upstream,
            .upstream = "fake://handler-upstream",
            .cache_hit = false,
        },
        // The second ask is the same decision answered from the cache, and a
        // cache hit names no upstream.
        .{
            .domain = "example.com",
            .group_id = 1,
            .group_name = "default",
            .action = .allow,
            .reason = .no_match,
            .route = .cache,
            .cache_hit = true,
        },
        .{
            .domain = "allowed.example.com",
            .group_id = 1,
            .group_name = "default",
            .action = .allow,
            .reason = .rule_allow_exact,
            .matched = "allowed.example.com",
            .route = .upstream,
            .upstream = "fake://handler-upstream",
            .cache_hit = false,
        },
        .{
            .domain = "blocked.example.com",
            .group_id = 1,
            .group_name = "default",
            .blocked = true,
            .action = .block,
            .reason = .rule_block_exact,
            .matched = "blocked.example.com",
            .route = .blocked,
        },
        .{
            .domain = "ads.example.net",
            .group_id = 1,
            .group_name = "default",
            .blocked = true,
            .action = .block,
            .reason = .blocklist_domain,
            .matched = "ads.example.net",
            .source_id = 11,
            .source_name = "list a",
            .route = .blocked,
        },
        .{
            .domain = "safe.example.net",
            .group_id = 1,
            .group_name = "default",
            .action = .allow,
            .reason = .blocklist_exception,
            .matched = "safe.example.net",
            .source_id = 11,
            .source_name = "list a",
            .route = .upstream,
            .upstream = "fake://handler-upstream",
            .cache_hit = false,
        },
    };

    var entries: [8]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, io, &entries);
    try testing.expectEqual(expected.len, logged.len);
    for (expected, logged) |row, *entry| try expectRow(row, entry);
}

test "a CNAME-uncloaked block names the target and the target's own decision" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try provenanceSnapshot(&.{blockRule("bad.example.org")});
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var chain_buf: [512]u8 = undefined;
    const chain = cnameChain(&chain_buf, query_bytes, &.{ "example.com", "bad.example.org" });

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = chain };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.sink = &sink;

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, t.io(), &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    // The row names the question the client asked; `cname_target` is what says
    // a chain was followed, and the reason is the target's own.
    try expectRow(.{
        .domain = "example.com",
        .group_id = 1,
        .group_name = "default",
        .blocked = true,
        .action = .block,
        .reason = .rule_block_exact,
        .matched = "bad.example.org",
        .cname_target = "bad.example.org",
        .route = .blocked,
    }, &logged[0]);
}

test "a safe-search rewrite records its target and stays an allowed query" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .groups = &.{.{ .name = "default", .safe_search = true }},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.sink = &sink;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, "www.google.com", .a, .in);

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query, &buf));

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, t.io(), &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    try expectRow(.{
        .domain = "www.google.com",
        .group_id = 1,
        .group_name = "default",
        .action = .allow,
        .reason = .no_match,
        .safe_search_target = "forcesafesearch.google.com",
        .route = .upstream,
        .upstream = "fake://handler-upstream",
        .cache_hit = false,
    }, &logged[0]);
}

test "a local record and a forward zone are explained with no upstream named" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var table = try records.Records.build(testing.allocator, &.{
        .{ .name = "nas.lan", .rtype = .a, .value = "192.168.1.10", .ttl = 60 },
    });
    defer table.deinit(testing.allocator);

    var zones = try forward_zones.Zones.build(testing.allocator, &.{
        .{ .zone = "lan.home", .resolver = "udp://127.0.0.1:1" },
    });
    defer zones.deinit(testing.allocator);

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    var snapshot = try provenanceSnapshot(&.{});
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    var tables: local_tables_mod.LocalTables = .{ .records = table, .zones = zones };
    h.local_tables = &tables;
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;
    h.sink = &sink;

    // The zone resolver is unreachable, so only a planted cache entry can
    // answer the zone query.
    var zone_query_buf: [512]u8 = undefined;
    const zone_query = queryFor(&zone_query_buf, 0x2222, "nas.lan.home", .a, .in);
    var zone_reply_buf: [512]u8 = undefined;
    const zone_p = try packet.parse(zone_query);
    var b = try packet.ResponseBuilder.init(&zone_reply_buf, zone_p.header, packet.firstQuestion(zone_p).?);
    try b.addAnswer(try name.fromText("nas.lan.home"), .a, .in, 300, "\xc0\xa8\x01\x0a");

    var key_buf: [dns_cache.max_key_len]u8 = undefined;
    const key = dns_cache.buildKey(
        &key_buf,
        "nas.lan.home",
        @intFromEnum(types.Type.a),
        @intFromEnum(types.Class.in),
        false,
        null,
    );
    try cache.put(
        std.Io.Clock.real.now(io).toSeconds(),
        key,
        b.finish(),
        .{ .ttl_seconds = 300, .negative = false },
    );

    var buf: [udp_limit_min]u8 = undefined;
    var local_query_buf: [512]u8 = undefined;
    _ = try expectReply(udp(&h, io, queryFor(&local_query_buf, 0x1111, "nas.lan", .a, .in), &buf));
    _ = try expectReply(udp(&h, io, zone_query, &buf));

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, io, &entries);
    try testing.expectEqual(@as(usize, 2), logged.len);

    try expectRow(.{
        .domain = "nas.lan",
        .group_id = 1,
        .group_name = "default",
        .action = .allow,
        .reason = .local_record,
        .route = .local,
    }, &logged[0]);
    try expectRow(.{
        .domain = "nas.lan.home",
        .group_id = 1,
        .group_name = "default",
        .action = .allow,
        .reason = .forward_zone,
        .forward_zone = "lan.home",
        .route = .cache,
        .cache_hit = true,
    }, &logged[1]);
}

/// The zone resolver's answer, distinct from the fake upstream's, so the reply
/// says which of the two produced it.
const zone_rdata = [4]u8{ 10, 9, 9, 7 };

/// A forward zone's resolver: a real UDP socket. `viaForwardZone` builds its
/// `ForwardClient` out of the zone's own resolver address, so no fake can stand
/// in for it — only a socket that answers produces a forward-zone row for an
/// exchange that succeeded. That is why the case below is gated behind
/// `-Dintegration` ("loopback sockets only") like every other socket-bound test
/// in the tree.
///
/// The loop ends when the receive is canceled, which is what `group.cancel`
/// does at the end of the case.
fn zoneResolver(io: std.Io, socket: *const std.Io.net.Socket, calls: *std.atomic.Value(u64)) void {
    var buf: [udp_limit_min]u8 = undefined;
    while (true) {
        const msg = socket.receive(io, &buf) catch return;
        _ = calls.fetchAdd(1, .monotonic);

        const request = packet.parse(msg.data) catch continue;
        const q = packet.firstQuestion(request) orelse continue;

        var reply_buf: [udp_limit_min]u8 = undefined;
        var b = packet.ResponseBuilder.init(&reply_buf, request.header, q) catch continue;
        b.addAnswer(q.name, .a, .in, 120, &zone_rdata) catch continue;
        socket.send(io, &msg.from, b.finish()) catch return;
    }
}

test "a forward zone that answers names its resolver, and its cache hit does not" {
    const build_options = @import("build_options");
    if (!build_options.integration) return error.SkipZigTest;

    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    const bind_address: std.Io.net.IpAddress = try .parse("127.0.0.1", 0);
    const socket = try bind_address.bind(io, .{ .mode = .dgram });
    defer socket.close(io);

    var calls: std.atomic.Value(u64) = .init(0);
    var group: std.Io.Group = .init;
    defer group.cancel(io);
    try group.concurrent(io, zoneResolver, .{ io, &socket, &calls });

    var resolver_text: [64]u8 = undefined;
    const resolver_url = try std.fmt.bufPrint(&resolver_text, "udp://127.0.0.1:{d}", .{
        socket.address.ip4.port,
    });

    var zones = try forward_zones.Zones.build(testing.allocator, &.{
        .{ .zone = "lan.home", .resolver = resolver_url },
    });
    defer zones.deinit(testing.allocator);

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    var snapshot = try provenanceSnapshot(&.{});
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    var tables: local_tables_mod.LocalTables = .{ .zones = zones };
    h.local_tables = &tables;
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;
    h.sink = &sink;
    // A real exchange over the loopback needs a deadline that survives a loaded
    // test machine; the fixture default bounds cases that expect no answer.
    h.policy.forward_read_timeout = .{ .raw = .fromSeconds(2), .clock = .awake };

    var buf: [udp_limit_min]u8 = undefined;
    var first_buf: [512]u8 = undefined;
    const first = try expectReply(udp(&h, io, queryFor(&first_buf, 0x3333, "nas.lan.home", .a, .in), &buf));
    const p = try packet.parse(first);
    try testing.expectEqual(zone_rdata, try record.rdataA(p.bytes, try firstAnswer(p)));

    var second_buf: [512]u8 = undefined;
    _ = try expectReply(udp(&h, io, queryFor(&second_buf, 0x3344, "nas.lan.home", .a, .in), &buf));
    try testing.expectEqual(@as(u64, 1), calls.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, io, &entries);
    try testing.expectEqual(@as(usize, 2), logged.len);

    // The exchange that happened names the resolver it reached; the hit that
    // replaces it names none, and only the route tells the two apart.
    try expectRow(.{
        .domain = "nas.lan.home",
        .group_id = 1,
        .group_name = "default",
        .action = .allow,
        .reason = .forward_zone,
        .forward_zone = "lan.home",
        .route = .forward_zone,
        .upstream = resolver_url,
        .cache_hit = false,
    }, &logged[0]);
    try expectRow(.{
        .domain = "nas.lan.home",
        .group_id = 1,
        .group_name = "default",
        .action = .allow,
        .reason = .forward_zone,
        .forward_zone = "lan.home",
        .route = .cache,
        .cache_hit = true,
    }, &logged[1]);
}

test "a query answered before filtering could apply says which step decided it" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var snapshot = try provenanceSnapshot(&.{blockRule("example.com")});
    defer snapshot.deinit();

    // Non-IN and paused both run against a published snapshot; the third has
    // none, so it names no group at all.
    var live: manager.Manager = undefined;
    fixtureManager(&live, &snapshot);
    var empty: manager.Manager = undefined;
    fixtureManager(&empty, undefined);
    empty.current = null;

    var paused: pause.Pause = .{};
    paused.pauseFor(0, null);

    const Case = struct {
        mgr: *manager.Manager,
        pause: ?*pause.Pause,
        qclass: types.Class,
        expected: ExpectedRow,
    };
    const cases = [_]Case{
        .{ .mgr = &live, .pause = null, .qclass = .ch, .expected = .{
            .domain = "example.com",
            .qclass = .ch,
            .group_id = 1,
            .group_name = "default",
            .action = .not_evaluated,
            .reason = .non_in_class,
            .route = .upstream,
            .upstream = "fake://handler-upstream",
            .cache_hit = false,
        } },
        .{ .mgr = &live, .pause = &paused, .qclass = .in, .expected = .{
            .domain = "example.com",
            .group_id = 1,
            .group_name = "default",
            .action = .not_evaluated,
            .reason = .paused,
            .route = .upstream,
            .upstream = "fake://handler-upstream",
            .cache_hit = false,
        } },
        .{ .mgr = &empty, .pause = null, .qclass = .in, .expected = .{
            .domain = "example.com",
            .action = .not_evaluated,
            .reason = .snapshot_unavailable,
            .route = .upstream,
            .upstream = "fake://handler-upstream",
            .cache_hit = false,
        } },
    };

    for (cases) |c| {
        var queue_buf: [4]logger_mod.Entry = undefined;
        var lg: logger_mod.Logger = .init(.{}, &queue_buf);
        var log_owner: logger_controller.Borrowed = .{};
        var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

        var fake: FakeUpstream = .{ .reply = response_bytes };
        var h_owner: upstream_owner.Borrowed = .{};
        var h = bare(h_owner.client(fake.client()));
        h.manager = c.mgr;
        h.pause = c.pause;
        h.sink = &sink;

        var query_buf: [512]u8 = undefined;
        const query = queryFor(&query_buf, 0x1234, "example.com", .a, c.qclass);
        var buf: [udp_limit_min]u8 = undefined;
        _ = try expectReply(udp(&h, io, query, &buf));

        var entries: [4]logger_mod.Entry = undefined;
        const logged = drainLog(&lg, io, &entries);
        try testing.expectEqual(@as(usize, 1), logged.len);
        try expectRow(c.expected, &logged[0]);
    }
}

test "a SERVFAIL is logged with the resolver that lost" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    // An upstream exchange that fails after naming its resolver.
    {
        var queue_buf: [4]logger_mod.Entry = undefined;
        var lg: logger_mod.Logger = .init(.{}, &queue_buf);
        var log_owner: logger_controller.Borrowed = .{};
        var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

        var fake: FakeUpstream = .{ .err = error.Timeout };
        var h_owner: upstream_owner.Borrowed = .{};
        var h = bare(h_owner.client(fake.client()));
        h.sink = &sink;

        var buf: [udp_limit_min]u8 = undefined;
        _ = try expectReply(udp(&h, io, query_bytes, &buf));

        var entries: [4]logger_mod.Entry = undefined;
        const logged = drainLog(&lg, io, &entries);
        try testing.expectEqual(@as(usize, 1), logged.len);
        try expectRow(.{
            .domain = "example.com",
            .rcode = @intFromEnum(types.Rcode.serv_fail),
            .action = .not_evaluated,
            .reason = .snapshot_unavailable,
            .route = .upstream,
            .upstream = "fake://handler-upstream",
            .cache_hit = false,
        }, &logged[0]);
    }

    // A forward zone whose own resolver never answers.
    {
        var zones = try forward_zones.Zones.build(testing.allocator, &.{
            .{ .zone = "lan.home", .resolver = "udp://127.0.0.1:1" },
        });
        defer zones.deinit(testing.allocator);

        var queue_buf: [4]logger_mod.Entry = undefined;
        var lg: logger_mod.Logger = .init(.{}, &queue_buf);
        var log_owner: logger_controller.Borrowed = .{};
        var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

        var fake: FakeUpstream = .{ .reply = response_bytes };
        var h_owner: upstream_owner.Borrowed = .{};
        var h = bare(h_owner.client(fake.client()));
        var tables: local_tables_mod.LocalTables = .{ .zones = zones };
        h.local_tables = &tables;
        h.sink = &sink;

        var query_buf: [512]u8 = undefined;
        const query = queryFor(&query_buf, 0x1234, "nas.lan.home", .a, .in);
        var buf: [udp_limit_min]u8 = undefined;
        _ = try expectReply(udp(&h, io, query, &buf));

        var entries: [4]logger_mod.Entry = undefined;
        const logged = drainLog(&lg, io, &entries);
        try testing.expectEqual(@as(usize, 1), logged.len);
        try expectRow(.{
            .domain = "nas.lan.home",
            .rcode = @intFromEnum(types.Rcode.serv_fail),
            .action = .allow,
            .reason = .forward_zone,
            .forward_zone = "lan.home",
            .route = .forward_zone,
            .upstream = "udp://127.0.0.1:1",
            .cache_hit = false,
        }, &logged[0]);
    }
}

// The remaining `servFail` seams are encoding failures, so each one is reached
// by a query whose answer does not fit the reply the pipeline has to build.
// Nothing here shrinks `response_buf`: `handle` asserts it holds at least
// `udp_limit_min` (:168) and `max_synthetic_len` proves a header plus any
// question fits that, so a smaller buffer would break the handler's contract
// rather than exercise a reachable failure.

/// The local name the encoding-failure cases answer for, and the arithmetic
/// that says how many A records a 512-byte reply holds for it. The builder
/// compresses nothing — `record.encode` writes every owner name in full — so an
/// answer costs the wire name plus the fixed record fields.
const local_owner = "nas.lan";
/// `\x03nas\x03lan\x00`: a length byte per label, plus the root label.
const local_owner_wire_len = local_owner.len + 2;
const local_question_len = local_owner_wire_len + 4;
/// Owner name + TYPE + CLASS + TTL + RDLENGTH + the four address bytes.
const local_a_answer_len = local_owner_wire_len + 10 + 4;
/// The most A records that fit beside the header and the echoed question.
const local_a_fit = (udp_limit_min - types.header_len - local_question_len) / local_a_answer_len;

comptime {
    const packed_len = types.header_len + local_question_len + local_a_fit * local_a_answer_len;
    // `local_a_fit` answers fit and one more does not: that pair reaches the
    // `writeAnswers` seam. A reply packed to `local_a_fit` then has no room for
    // the OPT echo, which is the seam after it.
    std.debug.assert(packed_len <= udp_limit_min);
    std.debug.assert(packed_len + local_a_answer_len > udp_limit_min);
    std.debug.assert(packed_len + opt_len > udp_limit_min);
}

/// `n` distinct A records for `local_owner`.
fn localARecords(comptime n: usize) [n]model.LocalRecord {
    var rows: [n]model.LocalRecord = undefined;
    for (&rows, 0..) |*row, i| row.* = .{
        .name = local_owner,
        .rtype = .a,
        .value = std.fmt.comptimePrint("10.0.0.{d}", .{i + 1}),
        .ttl = 60,
    };
    return rows;
}

const local_a_overflowing = localARecords(local_a_fit + 1);
const local_a_packed = localARecords(local_a_fit);

test "a local record that will not encode is a SERVFAIL that still says local" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var snapshot = try provenanceSnapshot(&.{});
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    // Two seams inside `viaLocal`: the answers overrun the reply, and — with the
    // answers packed to the last byte that fits — the OPT echo the query asked
    // for does not.
    const Case = struct { rows: []const model.LocalRecord, with_opt: bool };
    const cases = [_]Case{
        .{ .rows = &local_a_overflowing, .with_opt = false },
        .{ .rows = &local_a_packed, .with_opt = true },
    };

    for (cases) |c| {
        var table = try records.Records.build(testing.allocator, c.rows);
        defer table.deinit(testing.allocator);
        var tables: local_tables_mod.LocalTables = .{ .records = table };

        var queue_buf: [4]logger_mod.Entry = undefined;
        var lg: logger_mod.Logger = .init(.{}, &queue_buf);
        var log_owner: logger_controller.Borrowed = .{};
        var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

        var fake: FakeUpstream = .{ .reply = response_bytes };
        var h_owner: upstream_owner.Borrowed = .{};
        var h = bare(h_owner.client(fake.client()));
        h.manager = &mgr;
        h.local_tables = &tables;
        h.sink = &sink;

        var query_buf: [512]u8 = undefined;
        const query = if (c.with_opt)
            queryWithOptions(&query_buf, local_owner, &.{})
        else
            queryFor(&query_buf, 0x1111, local_owner, .a, .in);

        var buf: [udp_limit_min]u8 = undefined;
        _ = try expectReply(udp(&h, io, query, &buf));

        // The answer never went out, so nothing counts as a local answer — but
        // the row still says which step owned the query.
        try testing.expectEqual(@as(u64, 0), h.stats.local_answers.load(.monotonic));
        try testing.expectEqual(@as(usize, 0), fake.calls);

        var entries: [4]logger_mod.Entry = undefined;
        const logged = drainLog(&lg, io, &entries);
        try testing.expectEqual(@as(usize, 1), logged.len);
        try expectRow(.{
            .domain = local_owner,
            .rcode = @intFromEnum(types.Rcode.serv_fail),
            .group_id = 1,
            .group_name = "default",
            .action = .allow,
            .reason = .local_record,
            .route = .local,
        }, &logged[0]);
    }
}

test "a safe-search rewrite that will not fit is a SERVFAIL before any exchange" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .groups = &.{.{ .name = "default", .safe_search = true }},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.sink = &sink;

    // The padding option makes the rewritten question too large for the
    // 512-byte build buffer, which is the one way `outgoingQuery` returns null.
    var options_buf: [1024]u8 = undefined;
    var query_buf: [1024]u8 = undefined;
    const query = queryWithOptions(&query_buf, "www.google.com", paddingOption(&options_buf, 600));

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, io, query, &buf));
    try testing.expectEqual(@as(usize, 0), fake.calls);

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, io, &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    // The filter ran and allowed the name; the rewrite never became a target,
    // so the row names none and no resolver was attempted.
    try expectRow(.{
        .domain = "www.google.com",
        .rcode = @intFromEnum(types.Rcode.serv_fail),
        .group_id = 1,
        .group_name = "default",
        .action = .allow,
        .reason = .no_match,
        .route = .upstream,
        .cache_hit = false,
    }, &logged[0]);
}

/// An answer whose header promises a record the message does not carry.
/// `safeSearchReply` has to re-read the upstream answer to rebuild it under the
/// original question, and this is an answer it cannot read.
const unparseable_response =
    "\x12\x34\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00" ++
    "\x03www\x06google\x03com\x00\x00\x01\x00\x01";

test "a safe-search answer that will not rebuild is a SERVFAIL naming the resolver" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .groups = &.{.{ .name = "default", .safe_search = true }},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = unparseable_response };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.sink = &sink;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, "www.google.com", .a, .in);

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, io, query, &buf));
    try testing.expectEqual(@as(usize, 1), fake.calls);

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, io, &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    // The exchange happened, so the resolver is named and the rewrite that went
    // out is on the row — the failure is downstream of both.
    try expectRow(.{
        .domain = "www.google.com",
        .rcode = @intFromEnum(types.Rcode.serv_fail),
        .group_id = 1,
        .group_name = "default",
        .action = .allow,
        .reason = .no_match,
        .safe_search_target = "forcesafesearch.google.com",
        .route = .upstream,
        .upstream = "fake://handler-upstream",
        .cache_hit = false,
    }, &logged[0]);
}

/// A name at the wire maximum. A blocked reply for it carries the name twice —
/// once in the echoed question, once as the A record's owner — and the pair
/// does not fit a 512-byte UDP reply.
const max_length_name = "a" ** 63 ++ "." ++ "b" ** 63 ++ "." ++ "c" ** 63 ++ "." ++ "d" ** 61;

test "a blocked reply that will not encode is a SERVFAIL that still says blocked" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var snapshot = try provenanceSnapshot(&.{blockRule(max_length_name)});
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;
    h.sink = &sink;

    var query_buf: [512]u8 = undefined;
    const query = queryFor(&query_buf, 0x1234, max_length_name, .a, .in);

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, io, query, &buf));

    // No blocked answer reached the client, so none is counted — and the answer
    // the upstream would have given was never asked for either.
    try testing.expectEqual(@as(u64, 0), h.stats.blocked.load(.monotonic));
    try testing.expectEqual(@as(usize, 0), fake.calls);

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, io, &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    try expectRow(.{
        .domain = max_length_name,
        .rcode = @intFromEnum(types.Rcode.serv_fail),
        .blocked = true,
        .group_id = 1,
        .group_name = "default",
        .action = .block,
        .reason = .rule_block_exact,
        .matched = max_length_name,
        .route = .blocked,
    }, &logged[0]);
}

test "a protocol refusal that names a question is a row, not only a counter" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    var badvers_buf: [query_with_opt_len]u8 = undefined;
    const badvers = queryWithOptVersion(&badvers_buf, 1232, false, 1);

    var update: [query_bytes.len]u8 = query_bytes.*;
    std.mem.writeInt(u16, update[2..4], 0x2900, .big); // opcode update, RD set

    const Case = struct { query: []const u8, rcode: u12 };
    const cases = [_]Case{
        .{ .query = badvers, .rcode = edns.badvers },
        .{ .query = &update, .rcode = @intFromEnum(types.Rcode.not_imp) },
        .{ .query = query_with_bad_option, .rcode = @intFromEnum(types.Rcode.form_err) },
        .{ .query = query_with_named_opt, .rcode = @intFromEnum(types.Rcode.form_err) },
        .{ .query = query_with_opt_in_answer, .rcode = @intFromEnum(types.Rcode.form_err) },
    };

    for (cases) |c| {
        var queue_buf: [4]logger_mod.Entry = undefined;
        var lg: logger_mod.Logger = .init(.{}, &queue_buf);
        var log_owner: logger_controller.Borrowed = .{};
        var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

        var fake: FakeUpstream = .{ .reply = response_bytes };
        var h_owner: upstream_owner.Borrowed = .{};
        var h = bare(h_owner.client(fake.client()));
        h.sink = &sink;

        var buf: [udp_limit_min]u8 = undefined;
        _ = try expectReply(udp(&h, io, c.query, &buf));
        try testing.expectEqual(@as(usize, 0), fake.calls);

        var entries: [4]logger_mod.Entry = undefined;
        const logged = drainLog(&lg, io, &entries);
        try testing.expectEqual(@as(usize, 1), logged.len);
        try expectRow(.{
            .domain = "example.com",
            .rcode = c.rcode,
            .action = .not_evaluated,
            .reason = .protocol_error,
            .route = .rejected,
        }, &logged[0]);
    }
}

test "a query refused before any question is parsed stays a counter" {
    var t: TestIo = .init();
    defer t.deinit();

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.sink = &sink;

    const no_question = "\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00";
    const two_questions =
        "\x12\x34\x01\x00\x00\x02\x00\x00\x00\x00\x00\x00" ++
        "\x07example\x03com\x00\x00\x01\x00\x01" ++
        "\x07example\x03com\x00\x00\x1c\x00\x01";

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), no_question, &buf));
    _ = try expectReply(udp(&h, t.io(), two_questions, &buf));

    var entries: [4]logger_mod.Entry = undefined;
    try testing.expectEqual(@as(usize, 0), drainLog(&lg, t.io(), &entries).len);
    try testing.expectEqual(@as(u64, 2), h.stats.formerr.load(.monotonic));
}

test "an upstream identity carrying a credential is redacted before it is stored" {
    var t: TestIo = .init();
    defer t.deinit();

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    // `Endpoint.parse` rejects `@`, so this shape cannot come through the
    // production config — the redaction is asserted against an injected one.
    var fake: FakeUpstream = .{
        .reply = response_bytes,
        .identity = "https://user:hunter2@dns.example.net/dns-query?token=abc",
    };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.sink = &sink;

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, t.io(), &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    try testing.expectEqualStrings("https://dns.example.net", logged[0].upstream());
    try testing.expect(std.mem.indexOf(u8, logged[0].upstream(), "hunter2") == null);
    try testing.expect(std.mem.indexOf(u8, logged[0].upstream(), "token") == null);
}

test "the sink both streams and logs the query the handler answered" {
    var t: TestIo = .init();
    defer t.deinit();
    const io = t.io();

    const hub = try testing.allocator.create(sse.Hub);
    defer testing.allocator.destroy(hub);
    hub.init();

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), hub);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.sink = &sink;

    const id = hub.subscribe(io).?;
    defer hub.unsubscribe(io, id);

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, io, query_bytes, &buf));

    const streamed = hub.next(io, id).?;
    try testing.expectEqualStrings("example.com", streamed.domain());
    try testing.expectEqualStrings("192.168.1.50", streamed.clientIp());
    try testing.expectEqualStrings("fake://handler-upstream", streamed.upstream());
    try testing.expect(hub.next(io, id) == null);

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, io, &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    try testing.expectEqualStrings("example.com", logged[0].domain());
}

test "a refused query is counted and never logged" {
    var t: TestIo = .init();
    defer t.deinit();

    var limiter: rate_limiter.RateLimiter = try .init(
        testing.allocator,
        .{ .limit = 1, .window_seconds = 60 },
    );
    defer limiter.deinit();

    var queue_buf: [4]logger_mod.Entry = undefined;
    var lg: logger_mod.Logger = .init(.{}, &queue_buf);
    var log_owner: logger_controller.Borrowed = .{};
    var sink: query_sink.QuerySink = .init(log_owner.over(&lg), null);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.limiter = &limiter;
    h.sink = &sink;

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query_bytes, &buf));
    _ = try expectReply(udp(&h, t.io(), query_bytes, &buf));

    var entries: [4]logger_mod.Entry = undefined;
    const logged = drainLog(&lg, t.io(), &entries);
    try testing.expectEqual(@as(usize, 1), logged.len);
    try testing.expectEqual(@as(u64, 1), h.stats.refused.load(.monotonic));
}

test "safe search and an ecs strip compose into one outgoing query" {
    var t: TestIo = .init();
    defer t.deinit();

    var snapshot = try buildSnapshot(testing.allocator, .{
        .groups = &.{.{ .name = "default", .safe_search = true }},
    });
    defer snapshot.deinit();
    var mgr: manager.Manager = undefined;
    fixtureManager(&mgr, &snapshot);

    var ecs_buf: [11]u8 = undefined;
    var options_buf: [64]u8 = undefined;
    const ecs = ecsOption(&ecs_buf, 1);
    @memcpy(options_buf[0..cookie_option.len], cookie_option);
    @memcpy(options_buf[cookie_option.len..][0..ecs.len], ecs);

    var query_buf: [512]u8 = undefined;
    const query = queryWithOptions(
        &query_buf,
        "www.google.com",
        options_buf[0 .. cookie_option.len + ecs.len],
    );

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.manager = &mgr;

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query, &buf));

    const sent = try packet.parse(fake.sent());
    try testing.expectEqualSlices(
        u8,
        (try name.fromText("forcesafesearch.google.com")).wire(),
        packet.firstQuestion(sent).?.name.wire(),
    );

    const opt = try edns.parseOpt(fake.sent(), packet.findOptRecord(sent).?);
    try testing.expect((try edns.findOption(fake.sent(), opt, edns.ecs_option_code)) == null);
    try testing.expect((try edns.findOption(fake.sent(), opt, 10)) != null);
    try testing.expectEqual(@as(u64, 1), h.stats.safesearch_rewrites.load(.monotonic));
}

/// An RFC 7830 padding option (code 12) of `padding_len` zero bytes, written at
/// the start of `buf`. Long enough padding puts a query out of reach of the
/// 512-byte build buffer, which is what the rebuild paths fail on.
fn paddingOption(buf: []u8, padding_len: u16) []const u8 {
    std.mem.writeInt(u16, buf[0..2], 12, .big);
    std.mem.writeInt(u16, buf[2..4], padding_len, .big);
    @memset(buf[4..][0..padding_len], 0);
    return buf[0 .. 4 + padding_len];
}

/// A query whose OPT record is too large for the 512-byte build buffer to
/// rebuild, carrying a subnet behind the padding.
fn paddedEcsQuery(options_buf: *[1024]u8, query_buf: *[1024]u8) []const u8 {
    const padding = paddingOption(options_buf, 600);

    var ecs_buf: [11]u8 = undefined;
    const ecs = ecsOption(&ecs_buf, 1);
    @memcpy(options_buf[padding.len..][0..ecs.len], ecs);
    return queryWithOptions(query_buf, "example.com", options_buf[0 .. padding.len + ecs.len]);
}

test "a query too large to rebuild keeps its subnet and is counted" {
    var t: TestIo = .init();
    defer t.deinit();

    var options_buf: [1024]u8 = undefined;
    var query_buf: [1024]u8 = undefined;
    const query = paddedEcsQuery(&options_buf, &query_buf);

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query, &buf));

    // Ruling 13: the original goes out unchanged rather than the query failing.
    try testing.expectEqualSlices(u8, query, fake.sent());
    try testing.expectEqual(@as(u64, 1), h.stats.ecs_strip_failed.load(.monotonic));
}

test "a failed ecs strip keeps the query out of the cache in both directions" {
    var t: TestIo = .init();
    defer t.deinit();

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    // An entry stored under the subnet-free key every other client shares.
    var planted_buf: [512]u8 = undefined;
    const planted_p = try packet.parse(query_bytes);
    var b = try packet.ResponseBuilder.init(
        &planted_buf,
        planted_p.header,
        packet.firstQuestion(planted_p).?,
    );
    try b.addAnswer(try name.fromText("example.com"), .a, .in, 300, "\x09\x09\x09\x09");
    const planted = b.finish();

    var key_buf: [dns_cache.max_key_len]u8 = undefined;
    const global_key = dns_cache.buildKey(
        &key_buf,
        "example.com",
        @intFromEnum(types.Type.a),
        @intFromEnum(types.Class.in),
        false,
        null,
    );
    try cache.put(
        std.Io.Clock.real.now(t.io()).toSeconds(),
        global_key,
        planted,
        .{ .ttl_seconds = 300, .negative = false },
    );

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;

    var options_buf: [1024]u8 = undefined;
    var query_buf: [1024]u8 = undefined;
    const query = paddedEcsQuery(&options_buf, &query_buf);

    var buf: [udp_limit_min]u8 = undefined;
    const p = try packet.parse(try expectReply(udp(&h, t.io(), query, &buf)));

    // The subnet-free entry must not answer a query whose subnet went upstream.
    try testing.expectEqual(@as(u64, 1), h.stats.ecs_strip_failed.load(.monotonic));
    try testing.expectEqual(@as(u64, 0), h.stats.cache_hits.load(.monotonic));
    try testing.expectEqual(@as(usize, 1), fake.calls);
    try testing.expectEqual([4]u8{ 93, 184, 216, 34 }, try record.rdataA(p.bytes, try firstAnswer(p)));

    // Nor may the subnet-specific answer be stored where they would find it.
    try testing.expectEqual(@as(u32, 1), cache.len());
    _ = try expectReply(udp(&h, t.io(), query, &buf));
    try testing.expectEqual(@as(usize, 2), fake.calls);
    try testing.expectEqual(@as(u32, 1), cache.len());
}

test "forward mode refuses to cache a query carrying two subnets" {
    var t: TestIo = .init();
    defer t.deinit();

    var cache: dns_cache.DnsCache = try .init(testing.allocator, .{ .size = 8, .negative_ttl_max = 3600 });
    defer cache.deinit();

    var fake: FakeUpstream = .{ .reply = response_bytes };
    var h_owner: upstream_owner.Borrowed = .{};
    var h = bare(h_owner.client(fake.client()));
    h.policy.ecs_mode = .forward;
    h.cache = &cache;
    h.policy.negative_ttl_max = 3600;

    // Two ECS options: the key could only name one of the two subnets, and the
    // resolver is free to honour the other.
    var first_buf: [11]u8 = undefined;
    var second_buf: [11]u8 = undefined;
    var options_buf: [64]u8 = undefined;
    const first = ecsOption(&first_buf, 1);
    const second = ecsOption(&second_buf, 2);
    @memcpy(options_buf[0..first.len], first);
    @memcpy(options_buf[first.len..][0..second.len], second);

    var query_buf: [512]u8 = undefined;
    const query = queryWithOptions(&query_buf, "example.com", options_buf[0 .. first.len + second.len]);

    var buf: [udp_limit_min]u8 = undefined;
    _ = try expectReply(udp(&h, t.io(), query, &buf));
    try testing.expectEqual(@as(u32, 0), cache.len());
    try testing.expectEqual(@as(usize, 1), fake.calls);

    // Repeating it finds nothing to hit, in either subnet's key or the global one.
    _ = try expectReply(udp(&h, t.io(), query, &buf));
    try testing.expectEqual(@as(usize, 2), fake.calls);
    try testing.expectEqual(@as(u64, 0), h.stats.cache_hits.load(.monotonic));

    // One subnet is still cached normally.
    var single_buf: [512]u8 = undefined;
    const single = queryWithOptions(&single_buf, "example.com", first);
    _ = try expectReply(udp(&h, t.io(), single, &buf));
    try testing.expectEqual(@as(u32, 1), cache.len());
    _ = try expectReply(udp(&h, t.io(), single, &buf));
    try testing.expectEqual(@as(u64, 1), h.stats.cache_hits.load(.monotonic));
}
