//! The route table.
//!
//! Deliberately its own file: `router.zig` owns matching and dispatch, and the
//! entries are filled in by the session that writes the handlers (milestone 8,
//! session W8). Ruling 23's drift guards read `router.routes`, which is this
//! array re-exported, so the contract test and the router can never disagree
//! about what the server serves.
//!
//! Adding a route means adding one entry here — and documenting it in
//! openapi.yaml, which openapi.zig's tests and W10's drift guards enforce.
//! Nothing else in the web layer knows the path set.
//!
//! Policy columns restate two rulings as data: `auth = .open` is exactly
//! ruling 18's exemption list (monitoring endpoints, the contract, the login
//! itself), and `rate_limit = .exempt` is ruling 19's (Prometheus must never
//! see 429) plus the live stream, which holds one request across its whole
//! life and is bounded by the SSE per-address cap instead of the token
//! bucket. The static assets are ruling 18's remaining exemption; they are
//! not routes — the router sends unmatched non-`/api` paths to
//! `WebState.fallback` before any policy check.
//!
//! `policy` is the third such column, and milestone-20 ruling 7's contract:
//! under file authority the file is the sole declarative source, so a
//! `config_write` answers 403 and a `runtime_action` stays live. It has no
//! default value on purpose — a route added without a stated class must not
//! inherit one. Classification is per route, not per prefix:
//! `POST /api/blocklists/update` is a refresh, a `runtime_action`, while its
//! CRUD siblings write configuration. `DELETE /api/clients/{id}` is a
//! `runtime_action` here because deleting an *observed* row discards runtime
//! state the file never declared; the declared case needs a row read and the
//! clients handler answers it.

const router = @import("router.zig");

const auth = @import("handlers/auth.zig");
const blocklists = @import("handlers/blocklists.zig");
const certs = @import("handlers/certs.zig");
const clients = @import("handlers/clients.zig");
const config = @import("handlers/config.zig");
const diagnostics = @import("handlers/diagnostics.zig");
const groups = @import("handlers/groups.zig");
const health = @import("handlers/health.zig");
const live = @import("handlers/live.zig");
const local = @import("handlers/local.zig");
const lookup = @import("handlers/lookup.zig");
const metrics = @import("metrics.zig");
const openapi = @import("openapi.zig");
const overview = @import("handlers/overview.zig");
const pause = @import("handlers/pause.zig");
const queries = @import("handlers/queries.zig");
const rules = @import("handlers/rules.zig");
const settings = @import("handlers/settings.zig");
const upstreams = @import("handlers/upstreams.zig");
const version = @import("handlers/version.zig");

pub const table: []const router.RouteInfo = &.{
    // Monitoring and contract (ruling 18's open set, ruling 19's exemptions).
    .{ .method = .GET, .pattern = "/metrics", .auth = .open, .policy = .read, .handler = metrics.handle, .rate_limit = .exempt },
    .{ .method = .GET, .pattern = "/api/health", .auth = .open, .policy = .read, .handler = health.handle, .rate_limit = .exempt },
    .{ .method = .GET, .pattern = "/api/version", .auth = .open, .policy = .read, .handler = version.handle },
    .{ .method = .GET, .pattern = "/api/openapi.yaml", .auth = .open, .policy = .read, .handler = openapi.handle },

    // Authentication.
    .{ .method = .POST, .pattern = "/api/auth/login", .auth = .open, .policy = .runtime_action, .handler = auth.login },
    .{ .method = .POST, .pattern = "/api/auth/logout", .auth = .session, .policy = .runtime_action, .handler = auth.logout },

    // Query log, overview, live stream, lookup.
    .{ .method = .GET, .pattern = "/api/queries", .auth = .session, .policy = .read, .handler = queries.list },
    .{ .method = .GET, .pattern = "/api/queries/live", .auth = .session, .policy = .read, .handler = live.stream, .rate_limit = .exempt },
    // Listed after the literal `live`, which a linear first-match scan reaches
    // first — though `{id}` would refuse it anyway, since it captures a
    // positive integer and nothing else.
    .{ .method = .GET, .pattern = "/api/queries/{id}", .auth = .session, .policy = .read, .handler = queries.detail },
    .{ .method = .GET, .pattern = "/api/overview", .auth = .session, .policy = .read, .handler = overview.handle },
    .{ .method = .GET, .pattern = "/api/lookup", .auth = .session, .policy = .read, .handler = lookup.handle },

    // Diagnostics: the operational event log (milestone 27). The two purges are
    // `runtime_action` — the event log is runtime state no configuration file
    // declares, so file authority has nothing to say about deleting from it.
    .{ .method = .GET, .pattern = "/api/diagnostics", .auth = .session, .policy = .read, .handler = diagnostics.list },
    .{ .method = .DELETE, .pattern = "/api/diagnostics", .auth = .session, .policy = .runtime_action, .handler = diagnostics.purgeAll },
    .{ .method = .GET, .pattern = "/api/diagnostics/{id}", .auth = .session, .policy = .read, .handler = diagnostics.get },
    .{ .method = .DELETE, .pattern = "/api/diagnostics/{id}", .auth = .session, .policy = .runtime_action, .handler = diagnostics.purge },

    // Groups.
    .{ .method = .GET, .pattern = "/api/groups", .auth = .session, .policy = .read, .handler = groups.list },
    .{ .method = .POST, .pattern = "/api/groups", .auth = .session, .policy = .config_write, .handler = groups.create },
    .{ .method = .GET, .pattern = "/api/groups/{id}", .auth = .session, .policy = .read, .handler = groups.get },
    .{ .method = .PUT, .pattern = "/api/groups/{id}", .auth = .session, .policy = .config_write, .handler = groups.update },
    .{ .method = .DELETE, .pattern = "/api/groups/{id}", .auth = .session, .policy = .config_write, .handler = groups.remove },
    .{ .method = .GET, .pattern = "/api/groups/{id}/sources", .auth = .session, .policy = .read, .handler = groups.getSources },
    .{ .method = .PUT, .pattern = "/api/groups/{id}/sources", .auth = .session, .policy = .config_write, .handler = groups.putSources },

    // Blocklist sources. `/api/blocklists/update` is a literal segment; it
    // cannot collide with `{id}`, which only matches a positive integer.
    .{ .method = .GET, .pattern = "/api/blocklists", .auth = .session, .policy = .read, .handler = blocklists.list },
    .{ .method = .POST, .pattern = "/api/blocklists", .auth = .session, .policy = .config_write, .handler = blocklists.create },
    .{ .method = .POST, .pattern = "/api/blocklists/update", .auth = .session, .policy = .runtime_action, .handler = blocklists.refresh },
    .{ .method = .GET, .pattern = "/api/blocklists/{id}", .auth = .session, .policy = .read, .handler = blocklists.get },
    .{ .method = .PUT, .pattern = "/api/blocklists/{id}", .auth = .session, .policy = .config_write, .handler = blocklists.update },
    .{ .method = .DELETE, .pattern = "/api/blocklists/{id}", .auth = .session, .policy = .config_write, .handler = blocklists.remove },

    // Rules.
    .{ .method = .GET, .pattern = "/api/rules", .auth = .session, .policy = .read, .handler = rules.list },
    .{ .method = .POST, .pattern = "/api/rules", .auth = .session, .policy = .config_write, .handler = rules.create },
    .{ .method = .GET, .pattern = "/api/rules/{id}", .auth = .session, .policy = .read, .handler = rules.get },
    .{ .method = .PUT, .pattern = "/api/rules/{id}", .auth = .session, .policy = .config_write, .handler = rules.update },
    .{ .method = .DELETE, .pattern = "/api/rules/{id}", .auth = .session, .policy = .config_write, .handler = rules.remove },

    // Local records.
    .{ .method = .GET, .pattern = "/api/local-records", .auth = .session, .policy = .read, .handler = local.listRecords },
    .{ .method = .POST, .pattern = "/api/local-records", .auth = .session, .policy = .config_write, .handler = local.createRecord },
    .{ .method = .GET, .pattern = "/api/local-records/{id}", .auth = .session, .policy = .read, .handler = local.getRecord },
    .{ .method = .PUT, .pattern = "/api/local-records/{id}", .auth = .session, .policy = .config_write, .handler = local.updateRecord },
    .{ .method = .DELETE, .pattern = "/api/local-records/{id}", .auth = .session, .policy = .config_write, .handler = local.removeRecord },

    // Forward zones.
    .{ .method = .GET, .pattern = "/api/forward-zones", .auth = .session, .policy = .read, .handler = local.listZones },
    .{ .method = .POST, .pattern = "/api/forward-zones", .auth = .session, .policy = .config_write, .handler = local.createZone },
    .{ .method = .GET, .pattern = "/api/forward-zones/{id}", .auth = .session, .policy = .read, .handler = local.getZone },
    .{ .method = .PUT, .pattern = "/api/forward-zones/{id}", .auth = .session, .policy = .config_write, .handler = local.updateZone },
    .{ .method = .DELETE, .pattern = "/api/forward-zones/{id}", .auth = .session, .policy = .config_write, .handler = local.removeZone },

    // Clients (no POST — rows come from DNS activity or import, ruling 9).
    .{ .method = .GET, .pattern = "/api/clients", .auth = .session, .policy = .read, .handler = clients.list },
    .{ .method = .GET, .pattern = "/api/clients/{id}", .auth = .session, .policy = .read, .handler = clients.get },
    .{ .method = .PUT, .pattern = "/api/clients/{id}", .auth = .session, .policy = .config_write, .handler = clients.update },
    .{ .method = .DELETE, .pattern = "/api/clients/{id}", .auth = .session, .policy = .runtime_action, .handler = clients.remove },
    .{ .method = .GET, .pattern = "/api/client-prefixes", .auth = .session, .policy = .read, .handler = clients.listPrefixes },
    .{ .method = .PUT, .pattern = "/api/client-prefixes", .auth = .session, .policy = .config_write, .handler = clients.putPrefixes },

    // Upstreams (restart-required resource).
    .{ .method = .GET, .pattern = "/api/upstreams", .auth = .session, .policy = .read, .handler = upstreams.list },
    .{ .method = .POST, .pattern = "/api/upstreams", .auth = .session, .policy = .config_write, .handler = upstreams.create },
    .{ .method = .GET, .pattern = "/api/upstreams/{id}", .auth = .session, .policy = .read, .handler = upstreams.get },
    .{ .method = .PUT, .pattern = "/api/upstreams/{id}", .auth = .session, .policy = .config_write, .handler = upstreams.update },
    .{ .method = .DELETE, .pattern = "/api/upstreams/{id}", .auth = .session, .policy = .config_write, .handler = upstreams.remove },

    // Pause and settings.
    .{ .method = .GET, .pattern = "/api/pause", .auth = .session, .policy = .read, .handler = pause.get },
    .{ .method = .POST, .pattern = "/api/pause", .auth = .session, .policy = .runtime_action, .handler = pause.post },
    .{ .method = .GET, .pattern = "/api/settings", .auth = .session, .policy = .read, .handler = settings.get },
    .{ .method = .PUT, .pattern = "/api/settings", .auth = .session, .policy = .config_write, .handler = settings.put },

    // Configuration status: the authority and whether a restart is pending.
    .{ .method = .GET, .pattern = "/api/config/status", .auth = .session, .policy = .read, .handler = config.get },

    // Certificates (milestone-10 ruling 8).
    .{ .method = .POST, .pattern = "/api/certs/reload", .auth = .session, .policy = .runtime_action, .handler = certs.post },
};

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

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

test "the table carries every endpoint of the milestone" {
    try testing.expectEqual(@as(usize, 60), table.len);
}

test "no two entries claim the same method and pattern" {
    for (table, 0..) |a, i| {
        for (table[i + 1 ..]) |b| {
            if (a.method != b.method) continue;
            try testing.expect(!std.mem.eql(u8, a.pattern, b.pattern));
        }
    }
}

test "every pattern lives under /api except the Prometheus endpoint" {
    for (table) |route| {
        if (std.mem.eql(u8, route.pattern, "/metrics")) continue;
        try testing.expect(std.mem.startsWith(u8, route.pattern, "/api/"));
    }
}

test "the open set is exactly ruling 18's exemption list" {
    const open = [_][]const u8{
        "/metrics",
        "/api/health",
        "/api/version",
        "/api/openapi.yaml",
        "/api/auth/login",
    };
    var found: usize = 0;
    for (table) |route| {
        if (route.auth != .open) continue;
        found += 1;
        var listed = false;
        for (open) |pattern| listed = listed or std.mem.eql(u8, route.pattern, pattern);
        try testing.expect(listed);
    }
    try testing.expectEqual(open.len, found);
}

test "the limiter exemptions are the monitoring endpoints and the live stream" {
    const exempt = [_][]const u8{
        "/metrics",
        "/api/health",
        "/api/queries/live",
    };
    var found: usize = 0;
    for (table) |route| {
        if (route.rate_limit != .exempt) continue;
        found += 1;
        var listed = false;
        for (exempt) |pattern| listed = listed or std.mem.eql(u8, route.pattern, pattern);
        try testing.expect(listed);
    }
    try testing.expectEqual(exempt.len, found);
}

test "the config writes are exactly the declarative mutations" {
    const writes = [_][]const u8{
        "POST /api/groups",
        "PUT /api/groups/{id}",
        "DELETE /api/groups/{id}",
        "PUT /api/groups/{id}/sources",
        "POST /api/blocklists",
        "PUT /api/blocklists/{id}",
        "DELETE /api/blocklists/{id}",
        "POST /api/rules",
        "PUT /api/rules/{id}",
        "DELETE /api/rules/{id}",
        "POST /api/local-records",
        "PUT /api/local-records/{id}",
        "DELETE /api/local-records/{id}",
        "POST /api/forward-zones",
        "PUT /api/forward-zones/{id}",
        "DELETE /api/forward-zones/{id}",
        "PUT /api/clients/{id}",
        "PUT /api/client-prefixes",
        "POST /api/upstreams",
        "PUT /api/upstreams/{id}",
        "DELETE /api/upstreams/{id}",
        "PUT /api/settings",
    };
    try expectClass(.config_write, &writes);
}

test "the runtime actions are exactly ruling 7's list" {
    const actions = [_][]const u8{
        "POST /api/auth/login",
        "POST /api/auth/logout",
        "POST /api/blocklists/update",
        "DELETE /api/diagnostics",
        "DELETE /api/diagnostics/{id}",
        "DELETE /api/clients/{id}",
        "POST /api/pause",
        "POST /api/certs/reload",
    };
    try expectClass(.runtime_action, &actions);
}

test "every read is a GET and every GET is a read" {
    for (table) |route| {
        try testing.expectEqual(route.method == .GET, route.policy == .read);
    }
}

/// Asserts that the routes classified `policy` are exactly `expected`, each
/// written `METHOD /pattern`.
fn expectClass(policy: router.Policy, expected: []const []const u8) !void {
    var buf: [64]u8 = undefined;
    var found: usize = 0;
    for (table) |route| {
        if (route.policy != policy) continue;
        found += 1;
        const label = try std.fmt.bufPrint(&buf, "{t} {s}", .{ route.method, route.pattern });
        var listed = false;
        for (expected) |name| listed = listed or std.mem.eql(u8, label, name);
        if (!listed) {
            std.debug.print("{s} is {t}, and the list does not say so\n", .{ label, policy });
            return error.TestUnexpectedResult;
        }
    }
    try testing.expectEqual(expected.len, found);
}

test "item routes capture one id and collection routes capture none" {
    for (table) |route| {
        const captures = std.mem.count(u8, route.pattern, "{id}");
        try testing.expect(captures <= 1);
        if (captures == 1) {
            try testing.expect(route.method == .GET or route.method == .PUT or route.method == .DELETE);
        }
    }
}
