//! Integration tests for the bucketed trend-series queries + endpoints:
//! `repo::crawl_metrics::series`, `repo::page_analysis::analysis_series`, and
//! `GET /v1/admin/{crawler,analysis}/metrics/series`.

mod common;

use axum::http::StatusCode;
use axum::Router;
use sqlx::PgPool;
use tower::ServiceExt;
use uuid::Uuid;

use common::{body_json, get_with_cookie, harness, register_user};
use mangalord::repo::crawl_metrics::{self, Bucket};
use mangalord::repo::page_analysis;

async fn seed_admin(pool: &PgPool, app: &Router) -> String {
    let (username, cookie) = register_user(app).await;
    let u = mangalord::repo::user::find_by_username(pool, &username)
        .await
        .unwrap()
        .unwrap();
    mangalord::repo::user::set_is_admin_unchecked(pool, u.id, true)
        .await
        .unwrap();
    cookie
}

async fn insert_metric(pool: &PgPool, outcome: &str, duration_ms: i64, finished_at: &str) {
    sqlx::query(
        "INSERT INTO crawl_metrics (op, outcome, duration_ms, finished_at) \
         VALUES ('manga_detail', $1, $2, $3::timestamptz)",
    )
    .bind(outcome)
    .bind(duration_ms)
    .bind(finished_at)
    .execute(pool)
    .await
    .unwrap();
}

#[sqlx::test(migrations = "./migrations")]
async fn crawl_series_buckets_by_hour_and_day(pool: PgPool) {
    // Two rows in hour 09, one in hour 10 (same day).
    insert_metric(&pool, "ok", 100, "2026-06-18T09:10:00Z").await;
    insert_metric(&pool, "failed", 300, "2026-06-18T09:50:00Z").await;
    insert_metric(&pool, "ok", 200, "2026-06-18T10:05:00Z").await;

    let hourly = crawl_metrics::series(&pool, Bucket::Hour, None).await.unwrap();
    assert_eq!(hourly.len(), 2, "two hour buckets");
    // Ordered ascending; first bucket = hour 09 with 2 rows (1 ok / 1 failed).
    assert_eq!(hourly[0].n, 2);
    assert_eq!(hourly[0].ok, 1);
    assert_eq!(hourly[0].failed, 1);
    assert_eq!(hourly[0].avg_ms, Some(200.0)); // (100+300)/2
    assert_eq!(hourly[1].n, 1);

    let daily = crawl_metrics::series(&pool, Bucket::Day, None).await.unwrap();
    assert_eq!(daily.len(), 1, "all three collapse into one day bucket");
    assert_eq!(daily[0].n, 3);
    assert_eq!(daily[0].failed, 1);
}

#[sqlx::test(migrations = "./migrations")]
async fn crawl_series_respects_since_window(pool: PgPool) {
    insert_metric(&pool, "ok", 100, "2020-01-01T00:00:00Z").await; // ancient
    insert_metric(&pool, "ok", 100, "2026-06-18T10:00:00Z").await; // recent
    let since = chrono::DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
        .unwrap()
        .with_timezone(&chrono::Utc);
    let got = crawl_metrics::series(&pool, Bucket::Day, Some(since))
        .await
        .unwrap();
    assert_eq!(got.len(), 1, "only the in-window row");
}

async fn seed_page(pool: &PgPool) -> Uuid {
    let manga = Uuid::new_v4();
    let chapter = Uuid::new_v4();
    let page = Uuid::new_v4();
    sqlx::query("INSERT INTO mangas (id, title) VALUES ($1, 'M')")
        .bind(manga)
        .execute(pool)
        .await
        .unwrap();
    sqlx::query("INSERT INTO chapters (id, manga_id, number) VALUES ($1, $2, 1)")
        .bind(chapter)
        .bind(manga)
        .execute(pool)
        .await
        .unwrap();
    sqlx::query(
        "INSERT INTO pages (id, chapter_id, page_number, storage_key, content_type) \
         VALUES ($1, $2, 1, 'k', 'image/png')",
    )
    .bind(page)
    .bind(chapter)
    .execute(pool)
    .await
    .unwrap();
    page
}

async fn insert_analysis(pool: &PgPool, page_id: Uuid, status: &str, dur: i64, analyzed_at: &str) {
    sqlx::query(
        "INSERT INTO page_analysis (page_id, status, duration_ms, analyzed_at) \
         VALUES ($1, $2, $3, $4::timestamptz)",
    )
    .bind(page_id)
    .bind(status)
    .bind(dur)
    .bind(analyzed_at)
    .execute(pool)
    .await
    .unwrap();
}

#[sqlx::test(migrations = "./migrations")]
async fn analysis_series_splits_ok_failed_by_bucket(pool: PgPool) {
    let p1 = seed_page(&pool).await;
    let p2 = seed_page(&pool).await;
    let p3 = seed_page(&pool).await;
    insert_analysis(&pool, p1, "done", 500, "2026-06-18T09:00:00Z").await;
    insert_analysis(&pool, p2, "failed", 999, "2026-06-18T09:30:00Z").await;
    insert_analysis(&pool, p3, "done", 700, "2026-06-18T11:00:00Z").await;

    let hourly = page_analysis::analysis_series(&pool, Bucket::Hour, None)
        .await
        .unwrap();
    assert_eq!(hourly.len(), 2);
    assert_eq!(hourly[0].n, 2);
    assert_eq!(hourly[0].ok, 1);
    assert_eq!(hourly[0].failed, 1);
    // Mean duration is done-only → 500 (the failed 999 is excluded).
    assert_eq!(hourly[0].avg_ms, Some(500.0));
}

#[sqlx::test(migrations = "./migrations")]
async fn crawler_series_endpoint_shape_and_gating(pool: PgPool) {
    let h = harness(pool.clone());
    // Non-admin → 403.
    let (_u, plain) = register_user(&h.app).await;
    let resp = h
        .app
        .clone()
        .oneshot(get_with_cookie("/api/v1/admin/crawler/metrics/series", &plain))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);

    let cookie = seed_admin(&pool, &h.app).await;
    insert_metric(&pool, "ok", 100, "2026-06-18T09:10:00Z").await;
    let resp = h
        .app
        .clone()
        .oneshot(get_with_cookie(
            "/api/v1/admin/crawler/metrics/series?days=1",
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_json(resp).await;
    assert!(body["buckets"].is_array());

    // Bad bucket → 400.
    let resp = h
        .app
        .clone()
        .oneshot(get_with_cookie(
            "/api/v1/admin/crawler/metrics/series?bucket=year",
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}

#[sqlx::test(migrations = "./migrations")]
async fn analysis_series_endpoint_ok(pool: PgPool) {
    let h = harness(pool.clone());
    let cookie = seed_admin(&pool, &h.app).await;
    let resp = h
        .app
        .oneshot(get_with_cookie(
            "/api/v1/admin/analysis/metrics/series?days=7",
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_json(resp).await;
    assert!(body["buckets"].is_array());
}
