//! The `config.db` schema, verbatim from PLAN §11.2, plus the two table orders
//! every other storage session needs.
//!
//! The DDL text is data, not code: `migrations.zig` carries it as step 1 and
//! never edits it in place. A schema change is a *new* step with new DDL, so
//! this string stays byte-identical to PLAN §11.2 forever.

const std = @import("std");

/// Migration step 1. Multi-statement text — it goes through `db.Db.exec`,
/// never through `prepare`.
pub const ddl_v1: [:0]const u8 =
    \\CREATE TABLE schema_version (version INTEGER NOT NULL);
    \\
    \\CREATE TABLE groups (
    \\    id INTEGER PRIMARY KEY,
    \\    name TEXT NOT NULL UNIQUE,
    \\    safe_search INTEGER NOT NULL DEFAULT 0
    \\);
    \\INSERT OR IGNORE INTO groups (id, name) VALUES (1, 'default');
    \\
    \\CREATE TABLE clients (
    \\    id INTEGER PRIMARY KEY,
    \\    ip TEXT NOT NULL UNIQUE,            -- canonical text form (v4 dotted / v6 RFC 5952)
    \\    name TEXT,
    \\    group_id INTEGER NOT NULL REFERENCES groups(id),
    \\    hand_edited INTEGER NOT NULL DEFAULT 0,
    \\    first_seen INTEGER NOT NULL,
    \\    last_seen INTEGER NOT NULL
    \\);
    \\
    \\CREATE TABLE client_prefixes (
    \\    id INTEGER PRIMARY KEY,
    \\    prefix TEXT NOT NULL UNIQUE,        -- "192.168.1.0/24", "fd00:abcd::/48"
    \\    group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
    \\    priority INTEGER NOT NULL DEFAULT 100
    \\);
    \\
    \\CREATE TABLE upstreams (
    \\    id INTEGER PRIMARY KEY,
    \\    url TEXT NOT NULL UNIQUE,
    \\    priority INTEGER NOT NULL DEFAULT 100,
    \\    enabled INTEGER NOT NULL DEFAULT 1
    \\);
    \\
    \\CREATE TABLE blocklist_sources (
    \\    id INTEGER PRIMARY KEY,
    \\    url TEXT NOT NULL UNIQUE,
    \\    name TEXT NOT NULL,
    \\    enabled INTEGER NOT NULL DEFAULT 1,
    \\    is_suggested INTEGER NOT NULL DEFAULT 0,
    \\    last_updated INTEGER,
    \\    domain_count INTEGER NOT NULL DEFAULT 0,
    \\    wildcard_count INTEGER NOT NULL DEFAULT 0,
    \\    skipped_regex_count INTEGER NOT NULL DEFAULT 0,
    \\    checksum TEXT
    \\);
    \\
    \\CREATE TABLE group_sources (
    \\    group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
    \\    source_id INTEGER NOT NULL REFERENCES blocklist_sources(id) ON DELETE CASCADE,
    \\    PRIMARY KEY (group_id, source_id)
    \\);
    \\
    \\CREATE TABLE rules (
    \\    id INTEGER PRIMARY KEY,
    \\    group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
    \\    pattern TEXT NOT NULL,
    \\    kind TEXT NOT NULL CHECK(kind IN ('exact','wildcard')),
    \\    action TEXT NOT NULL CHECK(action IN ('allow','block')),
    \\    created_at INTEGER NOT NULL
    \\);
    \\
    \\CREATE TABLE local_records (
    \\    id INTEGER PRIMARY KEY,
    \\    name TEXT NOT NULL,
    \\    rtype TEXT NOT NULL CHECK(rtype IN ('A','AAAA','CNAME')),
    \\    value TEXT NOT NULL,
    \\    ttl INTEGER NOT NULL DEFAULT 300,
    \\    UNIQUE(name, rtype, value)
    \\);
    \\
    \\CREATE TABLE forward_zones (
    \\    id INTEGER PRIMARY KEY,
    \\    zone TEXT NOT NULL UNIQUE,
    \\    resolver TEXT NOT NULL              -- "udp://192.168.1.1:53"
    \\);
    \\
    \\CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
;

/// Child-before-parent. Used by import's wipe step; correct under
/// `foreign_keys = ON`.
///
/// `upstreams`, `local_records`, `forward_zones` and `settings` have no foreign
/// keys, so their position is free; `groups` and `blocklist_sources` must come
/// last, after every referrer. `schema_version` is deliberately absent — an
/// import must never erase the stamped migration version.
pub const delete_order = [_][]const u8{
    "group_sources",     "rules",         "client_prefixes", "clients",
    "upstreams",         "local_records", "forward_zones",   "settings",
    "blocklist_sources", "groups",
};

/// Every table whose emptiness defines "the database has never been configured"
/// (S5.2). `groups` is absent because migration step 1 seeds `(1, 'default')`,
/// so an empty database still holds one group row; `schema_version` is absent
/// for the same reason.
pub const content_tables = [_][]const u8{
    "clients",       "client_prefixes", "upstreams",     "blocklist_sources",
    "group_sources", "rules",           "local_records", "forward_zones",
    "settings",
};

const testing = std.testing;

test "delete_order lists every referrer before the table it references" {
    // The two parents in the schema. Every child that references them must be
    // deleted first, or `foreign_keys = ON` turns import's wipe into a
    // Constraint error inside the transaction.
    const referrers_of_groups = [_][]const u8{ "clients", "client_prefixes", "group_sources", "rules" };
    const referrers_of_sources = [_][]const u8{"group_sources"};

    try testing.expect(indexOf(&delete_order, "groups") != null);
    for (referrers_of_groups) |child| {
        try testing.expect(indexOf(&delete_order, child).? < indexOf(&delete_order, "groups").?);
    }
    for (referrers_of_sources) |child| {
        try testing.expect(indexOf(&delete_order, child).? < indexOf(&delete_order, "blocklist_sources").?);
    }
}

test "content_tables is delete_order without groups" {
    try testing.expectEqual(delete_order.len - 1, content_tables.len);
    for (content_tables) |name| {
        try testing.expect(indexOf(&delete_order, name) != null);
    }
    try testing.expect(indexOf(&content_tables, "groups") == null);
    try testing.expect(indexOf(&content_tables, "schema_version") == null);
}

fn indexOf(haystack: []const []const u8, needle: []const u8) ?usize {
    for (haystack, 0..) |item, i| {
        if (std.mem.eql(u8, item, needle)) return i;
    }
    return null;
}
