//! A2: `ExecutionLogRepository::summarize_for_app` — the read model behind the
//! observability dashboard. Seeds a known set of `execution_logs` rows for one
//! app and asserts the rollup: totals, error rate, latency mean + percentiles,
//! the by-status / by-source breakdowns, and the hourly series. Scoped to a
//! unique `app_id`, so it is unaffected by any other rows in a shared DB.
//! Skips cleanly when `DATABASE_URL` is unset.

use picloud_manager_core::repo::{ExecutionLogRepository, PostgresExecutionLogRepository};
use picloud_shared::AppId;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use uuid::Uuid;

async fn pool_or_skip() -> Option<PgPool> {
    let Ok(url) = std::env::var("DATABASE_URL") else {
        picloud_test_support::abort_if_db_required("metrics_summary");
        eprintln!("metrics_summary: DATABASE_URL unset — skipping");
        return None;
    };
    let pool = PgPoolOptions::new()
        .max_connections(2)
        .connect(&url)
        .await
        .expect("connect to DATABASE_URL");
    sqlx::migrate!("./migrations")
        .run(&pool)
        .await
        .expect("apply migrations");
    Some(pool)
}

async fn make_app(pool: &PgPool) -> Uuid {
    // `apps.group_id` is NOT NULL, so anchor the app under a fresh root group.
    let gslug = format!("metrics-g-{}", Uuid::new_v4());
    let group: (Uuid,) =
        sqlx::query_as("INSERT INTO groups (slug, name) VALUES ($1, $1) RETURNING id")
            .bind(&gslug)
            .fetch_one(pool)
            .await
            .expect("group insert");
    let slug = format!("metrics-{}", Uuid::new_v4());
    let row: (Uuid,) =
        sqlx::query_as("INSERT INTO apps (slug, name, group_id) VALUES ($1, $1, $2) RETURNING id")
            .bind(&slug)
            .bind(group.0)
            .fetch_one(pool)
            .await
            .expect("app insert");
    row.0
}

async fn seed(pool: &PgPool, app: Uuid, duration_ms: i32, status: &str, source: &str) {
    sqlx::query(
        "INSERT INTO execution_logs (app_id, request_id, duration_ms, status, source) \
         VALUES ($1, gen_random_uuid(), $2, $3, $4)",
    )
    .bind(app)
    .bind(duration_ms)
    .bind(status)
    .bind(source)
    .execute(pool)
    .await
    .expect("seed execution_logs row");
}

fn count_of(counts: &[picloud_manager_core::repo::MetricsCount], key: &str) -> i64 {
    counts.iter().find(|c| c.key == key).map_or(0, |c| c.count)
}

#[tokio::test]
async fn summarize_rolls_up_counts_error_rate_and_percentiles() {
    let Some(pool) = pool_or_skip().await else {
        return;
    };
    let app = make_app(&pool).await;

    // 3 successes (http), 1 error (kv), 1 timeout (cron). Durations chosen so
    // the percentiles are exact: sorted [10, 20, 30, 50, 100].
    seed(&pool, app, 10, "success", "http").await;
    seed(&pool, app, 20, "success", "http").await;
    seed(&pool, app, 30, "success", "http").await;
    seed(&pool, app, 100, "error", "kv").await;
    seed(&pool, app, 50, "timeout", "cron").await;

    let repo = PostgresExecutionLogRepository::new(pool.clone());
    let s = repo
        .summarize_for_app(AppId::from(app), 24)
        .await
        .expect("summarize");

    assert_eq!(s.total, 5, "total executions");
    // Every non-success terminal state counts as an error: error + timeout = 2.
    assert_eq!(s.errors, 2, "error count");
    assert!(
        (s.error_rate - 0.4).abs() < 1e-9,
        "error rate: {}",
        s.error_rate
    );
    // mean of [10,20,30,100,50] = 42.
    assert!(
        (s.avg_duration_ms - 42.0).abs() < 1e-6,
        "avg: {}",
        s.avg_duration_ms
    );
    // percentile_cont(0.5) of the 5 sorted values = 30.
    assert!(
        (s.p50_duration_ms - 30.0).abs() < 1e-6,
        "p50: {}",
        s.p50_duration_ms
    );
    // percentile_cont(0.95): 0.95*(n-1)=3.8 → interpolate 50..100 → 90.
    assert!(
        (s.p95_duration_ms - 90.0).abs() < 1e-6,
        "p95: {}",
        s.p95_duration_ms
    );

    assert_eq!(count_of(&s.by_status, "success"), 3);
    assert_eq!(count_of(&s.by_status, "error"), 1);
    assert_eq!(count_of(&s.by_status, "timeout"), 1);

    assert_eq!(count_of(&s.by_source, "http"), 3);
    assert_eq!(count_of(&s.by_source, "kv"), 1);
    assert_eq!(count_of(&s.by_source, "cron"), 1);

    // All rows land in the current hour bucket; the series sums back to the total.
    assert!(!s.series.is_empty(), "series has at least one bucket");
    assert_eq!(s.series.iter().map(|b| b.total).sum::<i64>(), 5);
    assert_eq!(s.series.iter().map(|b| b.errors).sum::<i64>(), 2);
}

#[tokio::test]
async fn summarize_of_an_empty_app_is_all_zero() {
    let Some(pool) = pool_or_skip().await else {
        return;
    };
    let app = make_app(&pool).await;
    let repo = PostgresExecutionLogRepository::new(pool.clone());
    let s = repo
        .summarize_for_app(AppId::from(app), 24)
        .await
        .expect("summarize");
    assert_eq!(s.total, 0);
    assert_eq!(s.errors, 0);
    assert!(
        (s.error_rate - 0.0).abs() < 1e-9,
        "no executions → zero rate"
    );
    assert!(s.series.is_empty());
}
