//! Mirrors `sdk-ts/scripts/db-checkout-checkin.ts`. End-to-end exercise of
//! `Database::checkout` / `Database::checkin` including the optimistic
//! concurrency conflict path.
//!
//! Run with:
//!   HEYO_API_KEY=... HEYO_DB_ID=db-... cargo test --test db_checkout_checkin -- --ignored --nocapture

mod common;

use std::time::{Duration, Instant};

use heyo_sdk::{
    CheckinOptions, Database, ExecOptions, HeyoError, SqlValue,
};
use tokio::time::sleep;

fn looks_gzipped(bytes: &[u8]) -> bool {
    bytes.len() >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b
}

async fn wait_for_version_bump(
    db: &Database,
    prior: i64,
    budget: Duration,
) -> Result<i64, String> {
    let deadline = Instant::now() + budget;
    let mut last_seen = prior;
    while Instant::now() < deadline {
        let r = db.checkout().await.map_err(|e| e.to_string())?;
        last_seen = r.data_version;
        if r.data_version > prior {
            return Ok(r.data_version);
        }
        sleep(Duration::from_millis(200)).await;
    }
    Err(format!(
        "data_version did not advance past {} within {:?} (last seen {})",
        prior, budget, last_seen
    ))
}

#[tokio::test]
#[ignore]
async fn db_checkout_checkin_round_trip() {
    common::load_dotenv();
    let Some(opts) = common::client_options() else {
        eprintln!("[ci] skipping — HEYO_API_KEY not set");
        return;
    };
    let Ok(db_id) = std::env::var("HEYO_DB_ID") else {
        eprintln!("[ci] skipping — HEYO_DB_ID not set");
        return;
    };

    println!("[ci] base={} db={}", common::base_url(), db_id);
    let db = Database::get(&db_id, opts).await.expect("get");

    // 1. Initial checkout.
    let first = db.checkout().await.expect("checkout-1");
    println!(
        "[checkout-1] data_version={} bytes={} gzipped={}",
        first.data_version,
        first.bytes.len(),
        looks_gzipped(&first.bytes)
    );

    // 2. Force a write to bump data_version.
    db.exec(
        "CREATE TABLE IF NOT EXISTS _checkout_test (id INTEGER PRIMARY KEY, ts TEXT)",
        Vec::<SqlValue>::new(),
        ExecOptions::default(),
    )
    .await
    .expect("create table");
    let ts = chrono_now();
    db.exec(
        "INSERT INTO _checkout_test (ts) VALUES (?)",
        vec![SqlValue::Text(ts.clone())],
        ExecOptions::default(),
    )
    .await
    .expect("insert");
    println!("[exec] inserted ts={}", ts);

    // 3. Wait for the worker to apply the bump.
    let bumped = wait_for_version_bump(&db, first.data_version, Duration::from_secs(5))
        .await
        .expect("wait_for_version_bump");
    println!(
        "[wait] data_version advanced {} → {}",
        first.data_version, bumped
    );

    // Stale checkin should be refused.
    let err = db
        .checkin(
            first.bytes.clone(),
            CheckinOptions {
                expected_version: Some(first.data_version),
                force: false,
            },
        )
        .await
        .expect_err("stale checkin should fail");
    match err {
        HeyoError::CheckinConflict { expected, current } => {
            println!(
                "[conflict] refused as expected (expected={:?}, current={}) ✓",
                expected, current
            );
        }
        other => panic!("expected CheckinConflict, got {:?}", other),
    }

    // 4. Re-checkout, then checkin happy-path.
    let fresh = db.checkout().await.expect("checkout-2");
    println!(
        "[checkout-2] data_version={} bytes={}",
        fresh.data_version,
        fresh.bytes.len()
    );
    let result = db
        .checkin(
            fresh.bytes.clone(),
            CheckinOptions {
                expected_version: Some(fresh.data_version),
                force: false,
            },
        )
        .await
        .expect("checkin");
    println!(
        "[checkin] success: data_version={} s3_key={} forced={} ✓",
        result.data_version, result.s3_key, result.forced
    );
    assert!(
        result.data_version > fresh.data_version,
        "checkin should bump data_version"
    );
}

/// Tiny RFC3339-ish "now" string; we don't want a chrono dep just for this.
fn chrono_now() -> String {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    format!("ts-{}", now.as_millis())
}
