//! A hermetic Postgres database per test.
//!
//! ## The invariant
//!
//! **`manager-core`'s claim loops are global by design, so a test must not share
//! a database with anything that runs them — or with anything that does DDL.**
//!
//! In production one instance owns one database, and `OutboxRepo::claim_due`,
//! `claim_ready_step` and the reclaimers deliberately take no `app_id`: one
//! dispatcher serves every app. The test suites were the only place that ever
//! violated the "one instance, one database" assumption, by pointing N
//! independent instances at one database. Two concrete bugs came from it:
//!
//! * `picloud/tests/*_e2e` — each test spawned a real dispatcher via `build_app`,
//!   so test A's dispatcher claimed test B's outbox row, then A finished and its
//!   server dropped mid-dispatch, stranding the claim. B polled until it timed
//!   out. `dispatcher_e2e` failed ~1 run in 3, never under `--test-threads=1`.
//!
//! * `manager-core/tests/atomic_write` — its fault injection does
//!   `CREATE TRIGGER ... ON outbox`, which takes an ACCESS EXCLUSIVE lock on a
//!   table other suites are concurrently inserting into. Intermittent failures
//!   under load, and the DDL was visible to every other suite while installed.
//!
//! The fix in both cases is the same, and it is the harness's job, not the
//! product's: do not weaken the dispatcher (app-scoping `claim_due` would be
//! wrong), do not shorten `PICLOUD_OUTBOX_CLAIM_TIMEOUT_SEC` to fit a 10s test
//! window (600s is right for a data plane where a script may run 300s). Give each
//! test its own database, so the harness matches production.
//!
//! ## How
//!
//! Replaying the migrations per test is far too slow, so the first caller builds a
//! migrated **template** database once and every test then clones it with
//! `CREATE DATABASE ... TEMPLATE`, which is a file copy. Names derive from
//! (suite, test), so they are stable: a test drops and recreates its own database
//! on entry. A crashed test therefore leaves nothing to clean up — the next run
//! reclaims the name — and there is no teardown to forget. Tests using this need
//! no `DELETE FROM` cleanup at all.
//!
//! Returns `None` when `DATABASE_URL` is unset, matching the existing
//! skip-cleanly-without-a-DB convention.

use sqlx::postgres::PgPoolOptions;
use sqlx::{Connection, Executor, PgConnection, PgPool};

/// The migrated database every test database is cloned from.
const TEMPLATE_DB: &str = "picloud_test_template";

/// Serializes template creation and `CREATE DATABASE ... TEMPLATE` across every
/// concurrently-running test *process*. Advisory locks are cluster-wide, so this
/// works across test binaries, not just across threads. It is needed because
/// Postgres refuses to clone a template that has an open connection.
const TEMPLATE_LOCK: i64 = 0x0070_6963_6C6F_7564;

/// Split a URL into (everything before the database name, query suffix) so we can
/// address a sibling database on the same server.
fn split_url(url: &str) -> Option<(String, String)> {
    let (root, tail) = url.rsplit_once('/')?;
    let query = match tail.split_once('?') {
        Some((_, q)) => format!("?{q}"),
        None => String::new(),
    };
    Some((root.to_string(), query))
}

/// Postgres identifiers cap at 63 bytes, so a long test name is truncated and
/// disambiguated by a hash of the full (suite, test) pair.
fn db_name(suite: &str, test: &str) -> String {
    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
    for b in suite
        .bytes()
        .chain(b"::".iter().copied())
        .chain(test.bytes())
    {
        hash ^= u64::from(b);
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    let safe: String = test
        .chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() {
                c.to_ascii_lowercase()
            } else {
                '_'
            }
        })
        .take(38)
        .collect();
    format!("pt_{safe}_{:08x}", hash & 0xffff_ffff)
}

/// The running test's name. libtest names each test's thread after the test, and
/// `#[tokio::test]` drives the future on that same thread (`block_on` polls on the
/// caller), so this is stable for both the current-thread and multi-thread
/// flavors. Falls back to a constant if a future harness stops naming them —
/// tests would then share a database again, which is merely the old behavior, so
/// the fallback cannot be worse than the status quo.
fn test_name() -> String {
    std::thread::current()
        .name()
        .unwrap_or("unnamed")
        .rsplit("::")
        .next()
        .unwrap_or("unnamed")
        .to_string()
}

/// A database private to this test. `suite` disambiguates same-named tests in
/// different binaries. `None` when `DATABASE_URL` is unset.
///
/// # Panics
/// If `DATABASE_URL` is set but unusable (cannot connect, cannot create the
/// template, cannot clone it) — a misconfigured integration environment should
/// fail loudly, not silently skip.
pub async fn test_db_url(suite: &str) -> Option<String> {
    let Ok(url) = std::env::var("DATABASE_URL") else {
        eprintln!("{suite}: DATABASE_URL unset — skipping");
        return None;
    };
    let (root, query) = split_url(&url).expect("DATABASE_URL must name a database");
    let name = db_name(suite, &test_name());

    let mut admin = PgConnection::connect(&url)
        .await
        .expect("connect to DATABASE_URL");

    // Held across BOTH the template build and the clone: Postgres will not clone a
    // template that has an open connection, and the migrating connection below is
    // exactly such a connection.
    sqlx::query("SELECT pg_advisory_lock($1)")
        .bind(TEMPLATE_LOCK)
        .execute(&mut admin)
        .await
        .expect("take the template lock");

    ensure_template(&mut admin, &root, &query).await;

    // A stable name means a crashed test leaves nothing behind: the next run
    // reclaims it. FORCE evicts any connection a previous run leaked.
    admin
        .execute(format!(r#"DROP DATABASE IF EXISTS "{name}" WITH (FORCE)"#).as_str())
        .await
        .expect("drop the previous test database");
    admin
        .execute(format!(r#"CREATE DATABASE "{name}" TEMPLATE "{TEMPLATE_DB}""#).as_str())
        .await
        .expect("clone the template");

    sqlx::query("SELECT pg_advisory_unlock($1)")
        .bind(TEMPLATE_LOCK)
        .execute(&mut admin)
        .await
        .expect("release the template lock");
    admin.close().await.ok();

    Some(format!("{root}/{name}{query}"))
}

/// A pool onto a database private to this test. See [`test_db_url`].
///
/// # Panics
/// If the private database cannot be created or connected to.
pub async fn test_pool(suite: &str) -> Option<PgPool> {
    test_pool_sized(suite, 5).await
}

/// [`test_pool`] with an explicit connection cap — for suites that drive many
/// concurrent writers at once (e.g. the quota races in `atomic_write`, which need
/// enough connections to actually contend for the advisory lock).
///
/// # Panics
/// If the private database cannot be created or connected to.
pub async fn test_pool_sized(suite: &str, max_connections: u32) -> Option<PgPool> {
    let url = test_db_url(suite).await?;
    Some(
        PgPoolOptions::new()
            .max_connections(max_connections)
            .connect(&url)
            .await
            .expect("connect to the test database"),
    )
}

/// Create the template if absent, then migrate it. Migrating on every call (not
/// only on creation) is what picks up a newly-added migration; it is a handful of
/// queries once the template is current. The caller must hold [`TEMPLATE_LOCK`],
/// and this must leave no connection open to the template.
async fn ensure_template(admin: &mut PgConnection, root: &str, query: &str) {
    let exists: Option<(i32,)> = sqlx::query_as("SELECT 1 FROM pg_database WHERE datname = $1")
        .bind(TEMPLATE_DB)
        .fetch_optional(&mut *admin)
        .await
        .expect("look up the template");
    if exists.is_none() {
        admin
            .execute(format!(r#"CREATE DATABASE "{TEMPLATE_DB}""#).as_str())
            .await
            .expect("create the template");
    }

    let mut tmpl = PgConnection::connect(&format!("{root}/{TEMPLATE_DB}{query}"))
        .await
        .expect("connect to the template");
    sqlx::migrate!("../manager-core/migrations")
        .run(&mut tmpl)
        .await
        .expect("migrate the template");
    // Must be closed before the clone — see the lock comment above.
    tmpl.close().await.expect("close the template connection");
}
