//! Test-only admin routes. **Compiled in always, but only registered when
//! `EVENTSNAP_TEST_MODE=1` is set in the environment.** The route returns a hard
//! 404 in production builds because [`crate::main`] skips registering the handler.
//!
//! These exist to give the Playwright E2E suite a quick "reset everything"
//! escape hatch without forcing tests to maintain raw SQL fixtures or spin up a
//! fresh database container per test.

use axum::extract::State;
use axum::http::StatusCode;

use crate::auth::middleware::RequireAdmin;
use crate::error::AppError;
use crate::state::AppState;

/// Truncates every event-scoped table, wipes media on disk, and reseeds the `config`
/// table: numeric values from the migration defaults, but every feature toggle forced
/// OFF (production seeds them ON — see the note at the reseed below). Requires an admin
/// JWT — even with `EVENTSNAP_TEST_MODE=1` it cannot be hit anonymously.
pub async fn truncate_all(
    State(state): State<AppState>,
    RequireAdmin(_auth): RequireAdmin,
) -> Result<StatusCode, AppError> {
    // Truncate in dependency order doesn't matter with CASCADE, but listing the
    // tables explicitly makes the blast radius obvious in code review.
    sqlx::query(
        r#"TRUNCATE
            comment_hashtag,
            upload_hashtag,
            hashtag,
            "like",
            comment,
            export_job,
            upload,
            session,
            "user",
            event,
            config
        RESTART IDENTITY CASCADE"#,
    )
    .execute(&state.pool)
    .await?;

    // Reseed config. The NUMERIC values mirror migrations 005/015/016/017/019; the BOOLEAN
    // toggles deliberately do NOT — migration 009 seeds every one of them `true`
    // (production), and this forces them `false` so the suite isn't fighting rate limits
    // and quotas it isn't testing.
    //
    // Be aware of what that costs: this runs as an auto-fixture before EVERY test, so no
    // test starts from production's config unless it explicitly turns a toggle back on
    // (02-upload/rate-limit, 07-adversarial/ddos, 01-auth/rate-limit-nat, …). That blind
    // spot is exactly why an entire class of per-IP limiter bugs went unnoticed: the
    // limiters were simply off. When adding a limiter or quota, add a spec that enables it.
    //
    // Kept in sync by hand because pulling SQL out of the migration files at runtime is
    // fragile — if you add a config key in a migration, add it here too.
    sqlx::query(
        r#"INSERT INTO config (key, value) VALUES
            ('max_image_size_mb',          '20'),
            ('max_video_size_mb',          '500'),
            ('upload_rate_per_hour',       '100'),
            ('feed_rate_per_min',          '60'),
            ('export_rate_per_day',        '3'),
            ('join_ip_rate_per_min',       '300'),
            ('recover_ip_rate_per_min',    '30'),
            ('social_rate_per_min',        '120'),
            ('quota_tolerance',            '0.75'),
            ('estimated_guest_count',      '100'),
            ('compression_concurrency',    '2'),
            ('rate_limits_enabled',        'false'),
            ('upload_rate_enabled',        'false'),
            ('feed_rate_enabled',          'false'),
            ('export_rate_enabled',        'false'),
            ('join_rate_enabled',          'false'),
            ('social_rate_enabled',        'false'),
            ('admin_login_rate_enabled',   'false'),
            ('quota_enabled',              'false'),
            ('storage_quota_enabled',      'false'),
            ('upload_count_quota_enabled', 'false'),
            ('privacy_note',               '')
        ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value"#,
    )
    .execute(&state.pool)
    .await?;

    // Wipe media directory. Best-effort: if it doesn't exist, that's fine.
    let _ = tokio::fs::remove_dir_all(&state.config.media_path).await;
    let _ = tokio::fs::create_dir_all(&state.config.media_path).await;

    // Wipe the export directory too. Exports moved OUT of media_path (CR2 fix), so
    // the media wipe above no longer covers them — without this a real export in
    // one test would leave Gallery.zip on disk and contaminate the next.
    let _ = tokio::fs::remove_dir_all(&state.config.export_path).await;
    let _ = tokio::fs::create_dir_all(&state.config.export_path).await;

    // The rate limiter holds an in-memory HashMap; clear it so a previous test's
    // counters don't leak into the next one.
    state.rate_limiter.clear();

    // The reseed above wrote the `config` table directly (bypassing patch_config), so
    // the cache must be invalidated too — otherwise the first request after a truncate
    // could serve the previous test's toggles.
    state.config_cache.invalidate();

    // The other two in-memory singletons that TRUNCATE used to leave standing.
    //
    // `disk_cache` holds a free-space reading for up to its TTL. TRUNCATE has just deleted every
    // uploaded file, which materially changes free space — so without this the next test can
    // compute a storage quota from the PREVIOUS test's disk. That was harmless only while quotas
    // were globally disabled in e2e (they no longer are: see specs/02-upload/quota.spec.ts, which
    // steers the per-user limit off `free_disk_bytes`), i.e. two holes were masking each other.
    state.disk_cache.invalidate();

    // `media_total` caches SUM(user.total_upload_bytes) for the upload gate's keepsake-headroom
    // check. TRUNCATE has just zeroed every one of those rows, so a surviving reading would make
    // the next test's first upload measure its headroom against the previous test's gallery —
    // and that gate REFUSES uploads, so the failure would look like a spurious quota rejection.
    state.media_total.invalidate();

    // `sse_tickets` maps a ticket to a session token hash. TRUNCATE deletes the sessions, so every
    // surviving ticket is a dangling reference to a user that no longer exists.
    state.sse_tickets.clear();

    // Invalidate any in-flight/queued compression task spawned by the previous test. Without this a
    // task still waiting on the concurrency semaphore wakes AFTER this wipe, fails to find its
    // (now-deleted) file, and broadcasts upload-error/upload-deleted into the NEXT test's SSE
    // stream. (Export workers are already inert across a truncate: they are epoch-guarded on the
    // event row, and truncate gives the event a fresh random UUID, so their writes match nothing.)
    state.compression.bump_generation();

    Ok(StatusCode::NO_CONTENT)
}

/// Returns whether the truncate endpoint is enabled. Used by the e2e harness
/// during global-setup to fail loud if the test backend was started without
/// `EVENTSNAP_TEST_MODE=1`.
pub fn is_test_mode() -> bool {
    std::env::var("EVENTSNAP_TEST_MODE").as_deref() == Ok("1")
}
