//! Integration tests for the reconcile diff queries:
//! `existing_source_keys` (dropped rows count as present) and
//! `sync_manga_keys_with_blocking_job` (pending/running/dead block).
//!
//! The browser-driven full walk in `reconcile::reconcile_missing` needs a
//! real `chromiumoxide::Browser`, so it is covered by the manual/E2E path;
//! the pure set-diff (`select_missing`) is unit-tested in `crawler::reconcile`.

use mangalord::crawler::jobs::{self, JobPayload};
use mangalord::repo::crawler;
use sqlx::PgPool;
use uuid::Uuid;

async fn ensure_target(pool: &PgPool) {
    sqlx::query(
        "INSERT INTO sources (id, name, base_url) VALUES ('target','T','http://x') \
         ON CONFLICT DO NOTHING",
    )
    .execute(pool)
    .await
    .unwrap();
}

/// Insert a `manga_sources` row (with its backing manga) under `target`.
async fn seed_source_manga(pool: &PgPool, key: &str, dropped: bool) {
    let manga_id = Uuid::new_v4();
    sqlx::query("INSERT INTO mangas (id, title) VALUES ($1, $2)")
        .bind(manga_id)
        .bind(format!("M {key}"))
        .execute(pool)
        .await
        .unwrap();
    sqlx::query(
        "INSERT INTO manga_sources (source_id, source_manga_key, manga_id, source_url, dropped_at) \
         VALUES ('target', $1, $2, $3, CASE WHEN $4 THEN now() ELSE NULL END)",
    )
    .bind(key)
    .bind(manga_id)
    .bind(format!("http://x/{key}"))
    .bind(dropped)
    .execute(pool)
    .await
    .unwrap();
}

async fn enqueue_sync_manga(pool: &PgPool, key: &str) -> Uuid {
    let res = jobs::enqueue(
        pool,
        &JobPayload::SyncManga {
            source_id: "target".into(),
            source_manga_key: key.into(),
            url: format!("http://x/{key}"),
            title: format!("M {key}"),
        },
    )
    .await
    .unwrap();
    match res {
        jobs::EnqueueResult::Inserted(id) => id,
        jobs::EnqueueResult::Skipped => panic!("expected insert"),
    }
}

async fn set_job_state(pool: &PgPool, id: Uuid, state: &str) {
    sqlx::query("UPDATE crawler_jobs SET state = $2::text WHERE id = $1")
        .bind(id)
        .bind(state)
        .execute(pool)
        .await
        .unwrap();
}

#[sqlx::test(migrations = "./migrations")]
async fn existing_source_keys_includes_dropped_rows(pool: PgPool) {
    ensure_target(&pool).await;
    seed_source_manga(&pool, "live", false).await;
    seed_source_manga(&pool, "dropped", true).await;

    let keys = crawler::existing_source_keys(&pool, "target").await.unwrap();
    assert!(keys.contains("live"));
    assert!(
        keys.contains("dropped"),
        "a dropped manga_sources row must still count as present"
    );
    assert_eq!(keys.len(), 2);
}

#[sqlx::test(migrations = "./migrations")]
async fn sync_manga_blocking_job_matches_pending_running_dead_only(pool: PgPool) {
    ensure_target(&pool).await;
    // One key per state.
    enqueue_sync_manga(&pool, "pending").await; // stays pending
    let running = enqueue_sync_manga(&pool, "running").await;
    let dead = enqueue_sync_manga(&pool, "dead").await;
    let done = enqueue_sync_manga(&pool, "done").await;
    let failed = enqueue_sync_manga(&pool, "failed").await;
    set_job_state(&pool, running, "running").await;
    set_job_state(&pool, dead, "dead").await;
    set_job_state(&pool, done, "done").await;
    set_job_state(&pool, failed, "failed").await;

    let blocked = crawler::sync_manga_keys_with_blocking_job(&pool, "target")
        .await
        .unwrap();
    assert!(blocked.contains("pending"));
    assert!(blocked.contains("running"));
    assert!(blocked.contains("dead"));
    assert!(!blocked.contains("done"), "done must not block re-enqueue");
    assert!(
        !blocked.contains("failed"),
        "failed (mid-backoff) must not block re-enqueue"
    );
    assert_eq!(blocked.len(), 3);
}

#[sqlx::test(migrations = "./migrations")]
async fn sync_manga_blocking_job_is_per_source(pool: PgPool) {
    ensure_target(&pool).await;
    sqlx::query(
        "INSERT INTO sources (id, name, base_url) VALUES ('other','O','http://y') \
         ON CONFLICT DO NOTHING",
    )
    .execute(&pool)
    .await
    .unwrap();
    enqueue_sync_manga(&pool, "shared").await;
    jobs::enqueue(
        &pool,
        &JobPayload::SyncManga {
            source_id: "other".into(),
            source_manga_key: "elsewhere".into(),
            url: "http://y/elsewhere".into(),
            title: "Elsewhere".into(),
        },
    )
    .await
    .unwrap();

    let blocked = crawler::sync_manga_keys_with_blocking_job(&pool, "target")
        .await
        .unwrap();
    assert!(blocked.contains("shared"));
    assert!(!blocked.contains("elsewhere"));
    assert_eq!(blocked.len(), 1);
}
