package api_test

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"testing/fstest"
	"time"

	"github.com/owncord/server/api"
	"github.com/owncord/server/auth"
	"github.com/owncord/server/db"
	"github.com/owncord/server/permissions"
)

// ─── Helpers ─────────────────────────────────────────────────────────────────

func newAPITestDB(t *testing.T) *db.DB {
	t.Helper()
	database, err := db.Open(":memory:")
	if err != nil {
		t.Fatalf("db.Open: %v", err)
	}
	t.Cleanup(func() { _ = database.Close() })

	migrFS := fstest.MapFS{
		"001_schema.sql": {Data: apiTestSchema},
	}
	if err := db.MigrateFS(database, migrFS); err != nil {
		t.Fatalf("MigrateFS: %v", err)
	}
	return database
}

// ok is a trivial handler that responds 200 OK to confirm the middleware
// passed the request through.
func ok(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
}

// bearerToken wraps an HTTP handler with an Authorization header bearing token.
func withBearer(req *http.Request, token string) *http.Request {
	req.Header.Set("Authorization", "Bearer "+token)
	return req
}

// ─── AuthMiddleware tests ─────────────────────────────────────────────────────

func TestAuthMiddleware_ValidToken(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "alice", "hash", 4)
	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)
	_, _ = database.CreateSession(context.Background(), uid, hash, "test", "127.0.0.1")

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("AuthMiddleware valid token status = %d, want %d", rr.Code, http.StatusOK)
	}
}

func TestAuthMiddleware_MissingToken(t *testing.T) {
	database := newAPITestDB(t)

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusUnauthorized {
		t.Errorf("AuthMiddleware no token status = %d, want 401", rr.Code)
	}
}

func TestAuthMiddleware_ValidAPIToken(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "botuser", "hash", 4)
	token, _ := auth.GenerateToken()
	if _, err := database.CreateAPIToken(context.Background(), uid, auth.HashToken(token), "ci", nil); err != nil {
		t.Fatalf("CreateAPIToken: %v", err)
	}

	var gotUserID int64
	h := api.AuthMiddleware(database)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if u, ok := r.Context().Value(api.UserKey).(*db.User); ok && u != nil {
			gotUserID = u.ID
		}
		// An API-token principal has no login session: SessionKey must be nil.
		if s, ok := r.Context().Value(api.SessionKey).(*db.Session); ok && s != nil {
			t.Errorf("expected nil session for API-token principal, got %+v", s)
		}
		w.WriteHeader(http.StatusOK)
	}))
	req := withBearer(httptest.NewRequest(http.MethodGet, "/", nil), token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("API token status = %d, want 200", rr.Code)
	}
	if gotUserID != uid {
		t.Errorf("API token authenticated as user %d, want %d", gotUserID, uid)
	}
}

func TestAuthMiddleware_RevokedAPIToken(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "botuser2", "hash", 4)
	token, _ := auth.GenerateToken()
	id, _ := database.CreateAPIToken(context.Background(), uid, auth.HashToken(token), "ci", nil)
	if _, err := database.RevokeAPIToken(context.Background(), id); err != nil {
		t.Fatalf("RevokeAPIToken: %v", err)
	}

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := withBearer(httptest.NewRequest(http.MethodGet, "/", nil), token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusUnauthorized {
		t.Errorf("revoked API token status = %d, want 401", rr.Code)
	}
}

func TestAuthMiddleware_InvalidToken(t *testing.T) {
	database := newAPITestDB(t)

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, "notarealtoken")
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusUnauthorized {
		t.Errorf("AuthMiddleware invalid token status = %d, want 401", rr.Code)
	}
}

func TestAuthMiddleware_ExpiredSession(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "bob", "hash", 4)
	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)

	// Insert an already-expired session.
	pastTime := time.Now().Add(-time.Hour).UTC().Format("2006-01-02 15:04:05")
	_, _ = database.ExecContext(context.Background(),
		`INSERT INTO sessions (user_id, token, device, ip_address, expires_at) VALUES (?, ?, ?, ?, ?)`,
		uid, hash, "test", "127.0.0.1", pastTime,
	)

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusUnauthorized {
		t.Errorf("AuthMiddleware expired session status = %d, want 401", rr.Code)
	}
}

func TestAuthMiddleware_MalformedAuthHeader(t *testing.T) {
	database := newAPITestDB(t)

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))

	cases := []string{
		"Token abc", // wrong scheme
		"Bearer",    // missing token after Bearer
		"abc",       // no space
	}
	for _, header := range cases {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.Header.Set("Authorization", header)
		rr := httptest.NewRecorder()
		h.ServeHTTP(rr, req)
		if rr.Code != http.StatusUnauthorized {
			t.Errorf("AuthMiddleware header=%q status = %d, want 401", header, rr.Code)
		}
	}
}

// TestAuthMiddleware_DanglingRoleUnauthorized pins the `role == nil` guard:
// GetRoleByID returns (nil, nil) for a role_id with no roles row, so without
// the guard a nil role reached the request context and the request only died
// later, at RequirePermission's own nil check (403) — or not at all on routes
// that have no RequirePermission.
func TestAuthMiddleware_DanglingRoleUnauthorized(t *testing.T) {
	database := newAPITestDB(t)

	// users.role_id has a FK to roles(id), so the dangling row can only be
	// created with FK enforcement momentarily off (db.Open pins the pool to a
	// single connection, so the pragma applies to the inserts that follow).
	if _, err := database.ExecContext(context.Background(), `PRAGMA foreign_keys=OFF`); err != nil {
		t.Fatalf("disable foreign keys: %v", err)
	}
	res, err := database.ExecContext(context.Background(),
		`INSERT INTO users (username, password, role_id) VALUES ('dangling', '$2a$12$fake', 999)`)
	if err != nil {
		t.Fatalf("insert dangling user: %v", err)
	}
	uid, _ := res.LastInsertId()
	if _, err := database.ExecContext(context.Background(), `PRAGMA foreign_keys=ON`); err != nil {
		t.Fatalf("re-enable foreign keys: %v", err)
	}

	token, _ := auth.GenerateToken()
	if _, err := database.ExecContext(context.Background(),
		`INSERT INTO sessions (user_id, token, device, ip_address, expires_at)
		 VALUES (?, ?, 'test', '127.0.0.1', '2099-01-01T00:00:00Z')`,
		uid, auth.HashToken(token),
	); err != nil {
		t.Fatalf("insert session: %v", err)
	}

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusUnauthorized {
		t.Errorf("AuthMiddleware dangling role status = %d, want 401", rr.Code)
	}
}

// ─── RequirePermission tests ──────────────────────────────────────────────────

func TestRequirePermission_Allowed(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "carol", "hash", 4) // Member role = 0x663
	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)
	_, _ = database.CreateSession(context.Background(), uid, hash, "test", "127.0.0.1")

	h := api.AuthMiddleware(database)(
		api.RequirePermission(permissions.SendMessages)(http.HandlerFunc(ok)),
	)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("RequirePermission allowed status = %d, want 200", rr.Code)
	}
}

func TestRequirePermission_Forbidden(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "dave", "hash", 4) // Member role = 0x663
	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)
	_, _ = database.CreateSession(context.Background(), uid, hash, "test", "127.0.0.1")

	h := api.AuthMiddleware(database)(
		api.RequirePermission(permissions.ManageRoles)(http.HandlerFunc(ok)),
	)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusForbidden {
		t.Errorf("RequirePermission forbidden status = %d, want 403", rr.Code)
	}
}

func TestRequirePermission_Administrator_Bypass(t *testing.T) {
	database := newAPITestDB(t)
	// Owner role (id=1) has permissions 0x7FFFFFFF which includes ADMINISTRATOR (0x40000000)
	uid, _ := database.CreateUser(context.Background(), "owner", "hash", 1)
	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)
	_, _ = database.CreateSession(context.Background(), uid, hash, "test", "127.0.0.1")

	// Any permission should pass for ADMINISTRATOR
	h := api.AuthMiddleware(database)(
		api.RequirePermission(permissions.ManageRoles)(http.HandlerFunc(ok)),
	)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("RequirePermission administrator bypass status = %d, want 200", rr.Code)
	}
}

// TestRequirePermission_MultiBitRequiresAllBits pins the one behaviour the
// HasServerPerm consolidation changed: a multi-bit mask is ALL-of, not any-of.
// The previous raw `role.Permissions&perm == 0` test returned 200 here because
// Member holds SendMessages, which was enough to make the mask non-zero.
func TestRequirePermission_MultiBitRequiresAllBits(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "multibit", "hash", 4) // Member role = 1635, has SendMessages, not ManageRoles
	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)
	_, _ = database.CreateSession(context.Background(), uid, hash, "test", "127.0.0.1")

	h := api.AuthMiddleware(database)(
		api.RequirePermission(permissions.SendMessages | permissions.ManageRoles)(http.HandlerFunc(ok)),
	)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusForbidden {
		t.Errorf("RequirePermission partial multi-bit mask status = %d, want 403", rr.Code)
	}
}

// ─── RateLimitMiddleware tests ────────────────────────────────────────────────

func TestRateLimitMiddleware_UnderLimit(t *testing.T) {
	limiter := auth.NewRateLimiter()

	h := api.RateLimitMiddleware(limiter, 5, time.Minute)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "10.0.0.1:1234"
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("RateLimitMiddleware under limit status = %d, want 200", rr.Code)
	}
}

func TestRateLimitMiddleware_OverLimit(t *testing.T) {
	limiter := auth.NewRateLimiter()
	limit := 3

	h := api.RateLimitMiddleware(limiter, limit, time.Minute)(http.HandlerFunc(ok))

	for range limit {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.RemoteAddr = "10.0.0.2:1234"
		rr := httptest.NewRecorder()
		h.ServeHTTP(rr, req)
	}

	// This next request should be rate-limited.
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "10.0.0.2:1234"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusTooManyRequests {
		t.Errorf("RateLimitMiddleware over limit status = %d, want 429", rr.Code)
	}
}

func TestRateLimitMiddleware_RetryAfterHeader(t *testing.T) {
	limiter := auth.NewRateLimiter()

	h := api.RateLimitMiddleware(limiter, 1, time.Minute)(http.HandlerFunc(ok))

	// Exhaust limit.
	for range 2 {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.RemoteAddr = "10.0.0.3:1234"
		rr := httptest.NewRecorder()
		h.ServeHTTP(rr, req)
	}

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "10.0.0.3:1234"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Header().Get("Retry-After") == "" {
		t.Error("RateLimitMiddleware: missing Retry-After header on 429 response")
	}
}

func TestRateLimitMiddleware_XRealIPIgnoredWithoutTrustedProxy(t *testing.T) {
	// Without trusted proxies configured, X-Real-IP must be ignored.
	// Each request with the same RemoteAddr host counts as the same IP regardless
	// of what the X-Real-IP header says.
	limiter := auth.NewRateLimiter()
	limit := 2

	h := api.RateLimitMiddleware(limiter, limit, time.Minute)(http.HandlerFunc(ok))

	// Two requests from RemoteAddr 10.0.0.99 with an attacker-supplied X-Real-IP.
	for range limit {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.Header.Set("X-Real-IP", "192.168.1.1") // forged; must be ignored
		req.RemoteAddr = "10.0.0.99:9999"
		rr := httptest.NewRecorder()
		h.ServeHTTP(rr, req)
	}

	// Third request from the same RemoteAddr should be blocked — rate key is
	// 10.0.0.99, not the forged 192.168.1.1.
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("X-Real-IP", "192.168.1.1")
	req.RemoteAddr = "10.0.0.99:9999"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusTooManyRequests {
		t.Errorf("RateLimitMiddleware no-trusted-proxy status = %d, want 429", rr.Code)
	}
}

// lockedBuffer is a goroutine-safe writer for capturing log output.
type lockedBuffer struct {
	mu  sync.Mutex
	buf bytes.Buffer
}

func (b *lockedBuffer) Write(p []byte) (int, error) {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.buf.Write(p)
}

func (b *lockedBuffer) String() string {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.buf.String()
}

// TestRateLimitMiddleware_InvalidCIDRWarnsAtConstructionNotPerRequest locks
// the W3-3a hoist: the trusted-proxy CIDR list is parsed once when the
// middleware is constructed — warning about invalid entries there — never on
// the per-request path.
func TestRateLimitMiddleware_InvalidCIDRWarnsAtConstructionNotPerRequest(t *testing.T) {
	logBuf := &lockedBuffer{}
	prev := slog.Default()
	slog.SetDefault(slog.New(slog.NewTextHandler(logBuf, nil)))
	defer slog.SetDefault(prev)

	limiter := auth.NewRateLimiter()
	h := api.RateLimitMiddleware(limiter, 100, time.Minute,
		[]string{"not-a-cidr", "10.0.0.0/8"})(http.HandlerFunc(ok))

	const warnMsg = "ignoring invalid CIDR entry"
	if got := strings.Count(logBuf.String(), warnMsg); got != 1 {
		t.Fatalf("invalid-CIDR warnings at construction = %d, want 1 (log: %q)",
			got, logBuf.String())
	}

	// The valid entry still works: X-Real-IP honoured from the trusted proxy.
	for range 3 {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.RemoteAddr = "10.0.0.1:9999"
		req.Header.Set("X-Real-IP", "203.0.113.77")
		rr := httptest.NewRecorder()
		h.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Fatalf("request status = %d, want 200", rr.Code)
		}
	}

	if got := strings.Count(logBuf.String(), warnMsg); got != 1 {
		t.Fatalf("invalid-CIDR warnings after 3 requests = %d, want 1 — CIDRs re-parsed on the request path (log: %q)",
			got, logBuf.String())
	}
}

func TestRateLimitMiddleware_XRealIPHonouredFromTrustedProxy(t *testing.T) {
	// With a trusted proxy configured, X-Real-IP from that proxy is used.
	limiter := auth.NewRateLimiter()
	limit := 2
	trustedCIDRs := []string{"10.0.0.0/8"}

	h := api.RateLimitMiddleware(limiter, limit, time.Minute, trustedCIDRs)(http.HandlerFunc(ok))

	// Two requests coming through trusted proxy 10.0.0.1, client IP 203.0.113.5.
	for range limit {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.Header.Set("X-Real-IP", "203.0.113.5")
		req.RemoteAddr = "10.0.0.1:9999"
		rr := httptest.NewRecorder()
		h.ServeHTTP(rr, req)
	}

	// Third request with same X-Real-IP from same trusted proxy — should be blocked.
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("X-Real-IP", "203.0.113.5")
	req.RemoteAddr = "10.0.0.1:9999"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusTooManyRequests {
		t.Errorf("RateLimitMiddleware trusted proxy X-Real-IP status = %d, want 429", rr.Code)
	}
}

// ─── Fix 2.10: Ban expiry in AuthMiddleware ───────────────────────────────────

// TestAuthMiddleware_BannedUserBlocked verifies that an actively banned user
// with no expiry cannot pass the auth middleware.
func TestAuthMiddleware_BannedUserBlocked(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "banneduser", "hash", 4)
	_ = database.BanUser(context.Background(), uid, "rule violation", nil) // permanent ban
	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)
	_, _ = database.CreateSession(context.Background(), uid, hash, "test", "127.0.0.1")

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusForbidden {
		t.Errorf("AuthMiddleware banned user status = %d, want 403", rr.Code)
	}
}

// TestAuthMiddleware_ExpiredBanAllowed verifies that a user whose ban has
// expired in the past can pass the auth middleware.
func TestAuthMiddleware_ExpiredBanAllowed(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "expbanned", "hash", 4)

	// Set ban with an expiry time in the past.
	past := time.Now().UTC().Add(-time.Hour)
	_ = database.BanUser(context.Background(), uid, "temp ban", &past)

	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)
	_, _ = database.CreateSession(context.Background(), uid, hash, "test", "127.0.0.1")

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("AuthMiddleware expired-ban user status = %d, want 200", rr.Code)
	}
}

// TestAuthMiddleware_ActiveTemporaryBanBlocked verifies that a user with a
// temporary ban whose expiry is in the future is still blocked.
func TestAuthMiddleware_ActiveTemporaryBanBlocked(t *testing.T) {
	database := newAPITestDB(t)
	uid, _ := database.CreateUser(context.Background(), "tempbanned", "hash", 4)

	// Set ban with an expiry time in the future.
	future := time.Now().UTC().Add(time.Hour)
	_ = database.BanUser(context.Background(), uid, "temp ban", &future)

	token, _ := auth.GenerateToken()
	hash := auth.HashToken(token)
	_, _ = database.CreateSession(context.Background(), uid, hash, "test", "127.0.0.1")

	h := api.AuthMiddleware(database)(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	withBearer(req, token)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusForbidden {
		t.Errorf("AuthMiddleware active temp-ban user status = %d, want 403", rr.Code)
	}
}

// ─── SecurityHeaders tests ───────────────────────────────────────────────────

func TestSecurityHeaders_AllHeadersPresent(t *testing.T) {
	h := api.SecurityHeaders(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	want := map[string]string{
		"X-Content-Type-Options":  "nosniff",
		"X-Frame-Options":         "DENY",
		"X-Xss-Protection":        "0",
		"Referrer-Policy":         "strict-origin-when-cross-origin",
		"Content-Security-Policy": "default-src 'self'",
		"Permissions-Policy":      "camera=(), microphone=(), geolocation=()",
		"Cache-Control":           "no-store",
	}
	for header, expected := range want {
		if got := rr.Header().Get(header); got != expected {
			t.Errorf("SecurityHeaders: %s = %q, want %q", header, got, expected)
		}
	}
}

func TestSecurityHeaders_PassesThrough(t *testing.T) {
	// Middleware must not swallow the response — downstream handler must be called.
	called := false
	h := api.SecurityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		w.WriteHeader(http.StatusTeapot)
	}))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	if !called {
		t.Error("SecurityHeaders: downstream handler was not called")
	}
	if rr.Code != http.StatusTeapot {
		t.Errorf("SecurityHeaders: status = %d, want 418", rr.Code)
	}
}

func TestSecurityHeaders_DoesNotOverrideExistingHeaders(t *testing.T) {
	// If a downstream handler sets its own CSP, SecurityHeaders should not clobber it
	// because it runs before the handler writes. The middleware sets headers first,
	// the handler can then override them — that is the correct layering.
	// This test just confirms the middleware itself sets all seven headers.
	h := api.SecurityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Handler overrides CSP after SecurityHeaders has already set it.
		w.Header().Set("Content-Security-Policy", "default-src 'none'")
		w.WriteHeader(http.StatusOK)
	}))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()

	h.ServeHTTP(rr, req)

	// The handler's override wins because it runs after the middleware sets the header.
	if got := rr.Header().Get("Content-Security-Policy"); got != "default-src 'none'" {
		t.Errorf("SecurityHeaders: handler CSP override = %q, want \"default-src 'none'\"", got)
	}
}

// ─── MaxBodySize tests ────────────────────────────────────────────────────────

func TestMaxBodySize_UnderLimit(t *testing.T) {
	// A body smaller than the limit must be read successfully by the handler.
	const limit = 10                   // bytes
	body := strings.NewReader("hello") // 5 bytes — under limit

	h := api.MaxBodySize(limit)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		data := make([]byte, 20)
		n, _ := r.Body.Read(data)
		if n != 5 {
			t.Errorf("MaxBodySize under limit: read %d bytes, want 5", n)
		}
		w.WriteHeader(http.StatusOK)
	}))

	req := httptest.NewRequest(http.MethodPost, "/", body)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("MaxBodySize under limit: status = %d, want 200", rr.Code)
	}
}

func TestMaxBodySize_ExactLimit(t *testing.T) {
	// A body exactly at the limit must be read without error.
	const limit = 5
	body := strings.NewReader("hello") // exactly 5 bytes

	h := api.MaxBodySize(limit)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		data := make([]byte, 10)
		n, _ := r.Body.Read(data)
		if n != 5 {
			t.Errorf("MaxBodySize exact limit: read %d bytes, want 5", n)
		}
		w.WriteHeader(http.StatusOK)
	}))

	req := httptest.NewRequest(http.MethodPost, "/", body)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("MaxBodySize exact limit: status = %d, want 200", rr.Code)
	}
}

func TestMaxBodySize_OverLimit(t *testing.T) {
	// Reading beyond the limit must return an error from MaxBytesReader.
	const limit = 5
	body := strings.NewReader("hello world") // 11 bytes — over limit

	var readErr error
	h := api.MaxBodySize(limit)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		data := make([]byte, 20)
		_, readErr = r.Body.Read(data)
		w.WriteHeader(http.StatusOK)
	}))

	req := httptest.NewRequest(http.MethodPost, "/", body)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if readErr == nil {
		t.Error("MaxBodySize over limit: expected read error, got nil")
	}
}

func TestMaxBodySize_NilBody(t *testing.T) {
	// GET requests with no body must pass through without panic.
	h := api.MaxBodySize(1024)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()

	// Must not panic.
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("MaxBodySize nil body: status = %d, want 200", rr.Code)
	}
}

func TestMaxBodySize_PassesThrough(t *testing.T) {
	// Downstream handler must be called and its status code preserved.
	h := api.MaxBodySize(1024)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusCreated)
	}))

	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("data"))
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusCreated {
		t.Errorf("MaxBodySize pass-through: status = %d, want 201", rr.Code)
	}
}

// ─── AdminIPRestrict tests ──────────────────────────────────────────────────

func TestAdminIPRestrict_AllowedCIDR(t *testing.T) {
	h := api.AdminIPRestrict([]string{"127.0.0.0/8"}, nil)(http.HandlerFunc(ok))

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "127.0.0.1:9999"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("AdminIPRestrict allowed CIDR status = %d, want 200", rr.Code)
	}
}

func TestAdminIPRestrict_BlockedCIDR(t *testing.T) {
	h := api.AdminIPRestrict([]string{"10.0.0.0/8"}, nil)(http.HandlerFunc(ok))

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "192.168.1.1:9999" // not in 10.0.0.0/8
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusForbidden {
		t.Errorf("AdminIPRestrict blocked CIDR status = %d, want 403", rr.Code)
	}
}

func TestAdminIPRestrict_EmptyAllowsAll(t *testing.T) {
	h := api.AdminIPRestrict(nil, nil)(http.HandlerFunc(ok))

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "203.0.113.1:9999"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("AdminIPRestrict empty list status = %d, want 200", rr.Code)
	}
}

func TestAdminIPRestrict_InvalidCIDR(t *testing.T) {
	// Invalid CIDR should fail closed: the entry is skipped at construction,
	// leaving a non-empty allowed list with zero parsed networks — nothing
	// matches, so access is denied.
	h := api.AdminIPRestrict([]string{"not-a-cidr"}, nil)(http.HandlerFunc(ok))

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "127.0.0.1:9999"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusForbidden {
		t.Errorf("AdminIPRestrict invalid CIDR status = %d, want 403", rr.Code)
	}
}

func TestAdminIPRestrict_MultipleCIDRs(t *testing.T) {
	h := api.AdminIPRestrict([]string{"10.0.0.0/8", "192.168.0.0/16"}, nil)(http.HandlerFunc(ok))

	// First CIDR matches.
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "10.1.2.3:9999"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusOK {
		t.Errorf("AdminIPRestrict multi-CIDR (10.x) status = %d, want 200", rr.Code)
	}

	// Second CIDR matches.
	req = httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "192.168.1.50:9999"
	rr = httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusOK {
		t.Errorf("AdminIPRestrict multi-CIDR (192.168.x) status = %d, want 200", rr.Code)
	}

	// Neither matches.
	req = httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "172.16.0.1:9999"
	rr = httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusForbidden {
		t.Errorf("AdminIPRestrict multi-CIDR (no match) status = %d, want 403", rr.Code)
	}
}

// ─── AdminIPRestrict proxy-aware tests (BUG-116) ─────────────────────────────

// TestAdminIPRestrict_TrustedProxy_UsesXForwardedFor verifies that when the
// connecting IP is a trusted proxy, the real client IP is extracted from
// X-Forwarded-For and checked against admin CIDRs.
func TestAdminIPRestrict_TrustedProxy_UsesXForwardedFor(t *testing.T) {
	// Admin allowed: only 203.0.113.0/24. Trusted proxy: 127.0.0.1.
	h := api.AdminIPRestrict(
		[]string{"203.0.113.0/24"},
		[]string{"127.0.0.0/8"},
	)(http.HandlerFunc(ok))

	// Request from proxy (127.0.0.1) with real client in XFF → allowed.
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "127.0.0.1:9999"
	req.Header.Set("X-Forwarded-For", "203.0.113.50")
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusOK {
		t.Errorf("trusted proxy + allowed XFF status = %d, want 200", rr.Code)
	}

	// Request from proxy with disallowed real client → blocked.
	req = httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "127.0.0.1:9999"
	req.Header.Set("X-Forwarded-For", "198.51.100.1")
	rr = httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusForbidden {
		t.Errorf("trusted proxy + blocked XFF status = %d, want 403", rr.Code)
	}
}

// TestAdminIPRestrict_TrustedProxy_UsesXRealIP verifies X-Real-IP is preferred
// over X-Forwarded-For when both are present from a trusted proxy.
func TestAdminIPRestrict_TrustedProxy_UsesXRealIP(t *testing.T) {
	h := api.AdminIPRestrict(
		[]string{"203.0.113.0/24"},
		[]string{"127.0.0.0/8"},
	)(http.HandlerFunc(ok))

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "127.0.0.1:9999"
	req.Header.Set("X-Real-IP", "203.0.113.50")
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusOK {
		t.Errorf("trusted proxy + X-Real-IP status = %d, want 200", rr.Code)
	}
}

// TestAdminIPRestrict_UntrustedProxy_IgnoresHeaders verifies that proxy headers
// are ignored when the connecting IP is NOT a trusted proxy.
func TestAdminIPRestrict_UntrustedProxy_IgnoresHeaders(t *testing.T) {
	h := api.AdminIPRestrict(
		[]string{"203.0.113.0/24"},
		[]string{"10.0.0.0/8"}, // only 10.x is trusted
	)(http.HandlerFunc(ok))

	// Untrusted proxy at 192.168.1.1 tries to spoof XFF.
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "192.168.1.1:9999"
	req.Header.Set("X-Forwarded-For", "203.0.113.50") // spoofed
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusForbidden {
		t.Errorf("untrusted proxy spoofed XFF status = %d, want 403 (should use RemoteAddr)", rr.Code)
	}
}

// TestAdminIPRestrict_ProxyCollapse_WithoutTrusted verifies the original bug:
// without trusted proxies, a proxy on localhost makes everything appear local.
func TestAdminIPRestrict_ProxyCollapse_WithoutTrusted(t *testing.T) {
	// Admin CIDR: private networks. No trusted proxies.
	h := api.AdminIPRestrict(
		[]string{"127.0.0.0/8", "10.0.0.0/8"},
		nil, // no trusted proxies
	)(http.HandlerFunc(ok))

	// External client behind nginx on localhost — RemoteAddr is 127.0.0.1.
	// XFF has the real external IP, but it's ignored (no trusted proxies).
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "127.0.0.1:9999"
	req.Header.Set("X-Forwarded-For", "198.51.100.1") // real external IP
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	// Without trusted proxies, 127.0.0.1 is used — passes the private CIDR check.
	// This is the documented limitation: operators MUST configure trusted_proxies.
	if rr.Code != http.StatusOK {
		t.Errorf("no trusted proxies, proxy on localhost status = %d, want 200 (known limitation)", rr.Code)
	}
}

// ─── SecurityHeadersWithTLS tests ───────────────────────────────────────────

func TestSecurityHeadersWithTLS_HSTS(t *testing.T) {
	h := api.SecurityHeadersWithTLS("auto")(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if got := rr.Header().Get("Strict-Transport-Security"); got == "" {
		t.Error("SecurityHeadersWithTLS: missing HSTS header when TLS enabled")
	}
}

func TestSecurityHeadersWithTLS_NoHSTSWithoutTLS(t *testing.T) {
	h := api.SecurityHeadersWithTLS("")(http.HandlerFunc(ok))
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if got := rr.Header().Get("Strict-Transport-Security"); got != "" {
		t.Errorf("SecurityHeadersWithTLS: unexpected HSTS header %q when TLS disabled", got)
	}
}

// ─── handleLiveKitHealth tests ──────────────────────────────────────────────

func TestLiveKitHealth_Healthy(t *testing.T) {
	h := api.HandleLiveKitHealthForTest(func(_ context.Context) (bool, error) {
		return true, nil
	})

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String())
	}

	var resp map[string]any
	_ = json.NewDecoder(rr.Body).Decode(&resp)
	if resp["status"] != "ok" {
		t.Errorf("status = %v, want ok", resp["status"])
	}
	if resp["livekit_reachable"] != true {
		t.Errorf("livekit_reachable = %v, want true", resp["livekit_reachable"])
	}
}

func TestLiveKitHealth_Unhealthy(t *testing.T) {
	h := api.HandleLiveKitHealthForTest(func(_ context.Context) (bool, error) {
		return false, fmt.Errorf("connection refused")
	})

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusServiceUnavailable {
		t.Fatalf("status = %d, want 503; body: %s", rr.Code, rr.Body.String())
	}

	var resp map[string]any
	_ = json.NewDecoder(rr.Body).Decode(&resp)
	if resp["status"] != "degraded" {
		t.Errorf("status = %v, want degraded", resp["status"])
	}
	if resp["livekit_reachable"] != false {
		t.Errorf("livekit_reachable = %v, want false", resp["livekit_reachable"])
	}
	if resp["error"] != "connection refused" {
		t.Errorf("error = %v, want 'connection refused'", resp["error"])
	}
}

func TestLiveKitHealth_UnhealthyNoError(t *testing.T) {
	h := api.HandleLiveKitHealthForTest(func(_ context.Context) (bool, error) {
		return false, nil
	})

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusServiceUnavailable {
		t.Fatalf("status = %d, want 503; body: %s", rr.Code, rr.Body.String())
	}

	var resp map[string]any
	_ = json.NewDecoder(rr.Body).Decode(&resp)
	if resp["error"] != "unknown" {
		t.Errorf("error = %v, want 'unknown'", resp["error"])
	}
}

// apiTestSchema is the full schema needed for all api tests (middleware,
// auth handler, and invite handler).
var apiTestSchema = []byte(`
CREATE TABLE IF NOT EXISTS roles (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL UNIQUE,
    color       TEXT,
    permissions INTEGER NOT NULL DEFAULT 0,
    position    INTEGER NOT NULL DEFAULT 0,
    is_default  INTEGER NOT NULL DEFAULT 0
);

INSERT OR IGNORE INTO roles (id, name, color, permissions, position, is_default) VALUES
    (1, 'Owner',     '#E74C3C', 2147483647, 100, 0),
    (2, 'Admin',     '#F39C12', 1073741823,  80, 0),
    (3, 'Moderator', '#3498DB', 1048575,     60, 0),
    (4, 'Member',    NULL,      1635,     40, 1);

CREATE TABLE IF NOT EXISTS users (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    username    TEXT    NOT NULL UNIQUE COLLATE NOCASE,
    password    TEXT    NOT NULL,
    avatar      TEXT,
    role_id     INTEGER NOT NULL DEFAULT 4 REFERENCES roles(id),
    totp_secret TEXT,
    status      TEXT    NOT NULL DEFAULT 'offline',
    created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
    last_seen   TEXT,
    banned      INTEGER NOT NULL DEFAULT 0,
    ban_reason  TEXT,
    ban_expires TEXT,
    identity_public_key TEXT
);

CREATE TABLE IF NOT EXISTS sessions (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token      TEXT    NOT NULL UNIQUE,
    device     TEXT,
    ip_address TEXT,
    created_at TEXT    NOT NULL DEFAULT (datetime('now')),
    last_used  TEXT    NOT NULL DEFAULT (datetime('now')),
    expires_at TEXT    NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);

CREATE TABLE IF NOT EXISTS api_tokens (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash   TEXT    NOT NULL UNIQUE,
    label        TEXT    NOT NULL DEFAULT '',
    created_at   TEXT    NOT NULL DEFAULT (datetime('now')),
    last_used_at TEXT,
    expires_at   TEXT,
    revoked_at   TEXT
);

CREATE TABLE IF NOT EXISTS invites (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    code        TEXT    NOT NULL UNIQUE,
    created_by  INTEGER NOT NULL REFERENCES users(id),
    redeemed_by INTEGER REFERENCES users(id),
    max_uses    INTEGER,
    use_count   INTEGER NOT NULL DEFAULT 0,
    expires_at  TEXT,
    created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
    revoked     INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_invites_code ON invites(code);

CREATE TABLE IF NOT EXISTS settings (
	key   TEXT PRIMARY KEY,
	value TEXT NOT NULL
);

INSERT OR IGNORE INTO settings (key, value) VALUES
	('require_2fa', 'false'),
	('registration_open', 'true');

CREATE TABLE IF NOT EXISTS channels (
    id               INTEGER PRIMARY KEY AUTOINCREMENT,
    name             TEXT    NOT NULL,
    type             TEXT    NOT NULL DEFAULT 'text',
    category         TEXT,
    topic            TEXT,
    position         INTEGER NOT NULL DEFAULT 0,
    slow_mode        INTEGER NOT NULL DEFAULT 0,
    archived         INTEGER NOT NULL DEFAULT 0,
    created_at       TEXT    NOT NULL DEFAULT (datetime('now')),
    voice_max_users  INTEGER NOT NULL DEFAULT 0,
    voice_quality    TEXT,
    mixing_threshold INTEGER,
    voice_max_video  INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS channel_overrides (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    role_id    INTEGER NOT NULL REFERENCES roles(id)    ON DELETE CASCADE,
    allow      INTEGER NOT NULL DEFAULT 0,
    deny       INTEGER NOT NULL DEFAULT 0,
    UNIQUE(channel_id, role_id)
);

CREATE TABLE IF NOT EXISTS messages (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    user_id    INTEGER NOT NULL REFERENCES users(id),
    content    TEXT    NOT NULL,
    reply_to   INTEGER REFERENCES messages(id) ON DELETE SET NULL,
    edited_at  TEXT,
    deleted    INTEGER NOT NULL DEFAULT 0,
    pinned     INTEGER NOT NULL DEFAULT 0,
    timestamp  TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS dm_participants (
    channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    PRIMARY KEY (channel_id, user_id)
);

CREATE TABLE IF NOT EXISTS dm_open_state (
    user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    opened_at  TEXT NOT NULL DEFAULT (datetime('now')),
    PRIMARY KEY (user_id, channel_id)
);

CREATE TABLE IF NOT EXISTS reactions (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    emoji      TEXT    NOT NULL,
    created_at TEXT    NOT NULL DEFAULT (datetime('now')),
    UNIQUE(message_id, user_id, emoji)
);

CREATE TABLE IF NOT EXISTS read_states (
    user_id         INTEGER NOT NULL REFERENCES users(id)    ON DELETE CASCADE,
    channel_id      INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    last_message_id INTEGER NOT NULL DEFAULT 0,
    mention_count   INTEGER NOT NULL DEFAULT 0,
    PRIMARY KEY (user_id, channel_id)
);

CREATE TABLE IF NOT EXISTS audit_log (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    actor_id    INTEGER NOT NULL REFERENCES users(id),
    action      TEXT    NOT NULL,
    target_type TEXT    NOT NULL DEFAULT '',
    target_id   INTEGER NOT NULL DEFAULT 0,
    detail      TEXT    NOT NULL DEFAULT '',
    created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
);
`)
