//! Integration tests for the analysis worker's *vision readiness gate*.
//!
//! When a readiness probe is wired in and reports "not ready" (the vision
//! container is stopped, or running but the model is still loading), the
//! worker must NOT lease any `analyze_page` job. Leasing increments
//! `attempts` in SQL and a down vision then burns all retries and writes a
//! permanent `failed` row — poisoning the queue while the autoscaler is
//! mid-cold-start. So a not-ready gate must leave jobs untouched
//! (`state='pending'`, `attempts=0`, no `page_analysis` row) and resume the
//! instant the probe flips to ready.

use std::sync::Arc;
use std::time::Duration;

use mangalord::analysis::daemon::{
    self,
    test_support::{CountingDispatcher, ToggleReadiness},
    AnalysisDaemonConfig,
};
use mangalord::repo::page_analysis;
use sqlx::PgPool;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;

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

async fn job_row(pool: &PgPool, page_id: Uuid) -> (String, i32) {
    sqlx::query_as(
        "SELECT state, attempts FROM crawler_jobs \
          WHERE payload->>'kind' = 'analyze_page' AND payload->>'page_id' = $1",
    )
    .bind(page_id.to_string())
    .fetch_one(pool)
    .await
    .unwrap()
}

async fn wait_for_state(pool: &PgPool, page_id: Uuid, want: &str) {
    let result = tokio::time::timeout(Duration::from_secs(8), async {
        loop {
            if job_row(pool, page_id).await.0 == want {
                return;
            }
            tokio::time::sleep(Duration::from_millis(50)).await;
        }
    })
    .await;
    assert!(result.is_ok(), "job for {page_id} never reached state {want}");
}

fn spawn_with(
    pool: &PgPool,
    dispatcher: Arc<CountingDispatcher>,
    readiness: Option<Arc<dyn daemon::VisionReadiness>>,
) -> daemon::AnalysisDaemonHandle {
    daemon::spawn(
        pool.clone(),
        CancellationToken::new(),
        AnalysisDaemonConfig {
            dispatcher,
            workers: 1,
            job_timeout: Duration::from_secs(5),
            events: Arc::new(mangalord::analysis::events::AnalysisEvents::new()),
            readiness,
        },
    )
}

#[sqlx::test(migrations = "./migrations")]
async fn not_ready_gate_never_leases_or_fails_the_page(pool: PgPool) {
    let page_id = seed_page(&pool).await;
    page_analysis::enqueue_for_page(&pool, page_id, false)
        .await
        .unwrap();

    let dispatcher = CountingDispatcher::ok();
    let readiness = ToggleReadiness::new(false);
    let handle = spawn_with(&pool, dispatcher.clone(), Some(readiness.clone()));

    // Give the worker several poll cycles to (wrongly) lease if the gate is
    // broken.
    tokio::time::sleep(Duration::from_millis(800)).await;
    handle.shutdown().await;

    assert_eq!(dispatcher.call_count(), 0, "must not dispatch while not ready");
    let (state, attempts) = job_row(&pool, page_id).await;
    assert_eq!(state, "pending", "job must stay pending while vision is down");
    assert_eq!(attempts, 0, "job must not be leased (attempts must stay 0)");
    assert!(
        page_analysis::load(&pool, page_id).await.unwrap().is_none(),
        "no failed/any page_analysis row may be written while not ready"
    );
}

#[sqlx::test(migrations = "./migrations")]
async fn resumes_when_readiness_flips_to_ready(pool: PgPool) {
    let page_id = seed_page(&pool).await;
    page_analysis::enqueue_for_page(&pool, page_id, false)
        .await
        .unwrap();

    let dispatcher = CountingDispatcher::ok();
    let readiness = ToggleReadiness::new(false);
    let handle = spawn_with(&pool, dispatcher.clone(), Some(readiness.clone()));

    // Confirm it is parked, then open the gate.
    tokio::time::sleep(Duration::from_millis(300)).await;
    assert_eq!(dispatcher.call_count(), 0);
    readiness.set(true);

    wait_for_state(&pool, page_id, "done").await;
    handle.shutdown().await;
    assert_eq!(dispatcher.call_count(), 1, "must process once ready");
}

#[sqlx::test(migrations = "./migrations")]
async fn no_gate_processes_as_before(pool: PgPool) {
    // readiness = None preserves today's behavior for always-on endpoints.
    let page_id = seed_page(&pool).await;
    page_analysis::enqueue_for_page(&pool, page_id, false)
        .await
        .unwrap();

    let dispatcher = CountingDispatcher::ok();
    let handle = spawn_with(&pool, dispatcher.clone(), None);
    wait_for_state(&pool, page_id, "done").await;
    handle.shutdown().await;
    assert_eq!(dispatcher.call_count(), 1);
}
