//! Integration tests for the admin audit-log viewer: the `repo::admin_audit`
//! list query (filters, username join, NULL actor) and the
//! `GET /v1/admin/audit` endpoint (admin-gating, shape, filter params).

mod common;

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

use common::{body_json, get_with_cookie, harness, register_user};
use mangalord::repo::admin_audit::{self, AuditFilter};

async fn seed_admin(pool: &PgPool, app: &Router) -> (Uuid, 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();
    (u.id, cookie)
}

/// Insert an audit row with an explicit `at` so ordering/since are testable.
async fn insert_audit(
    pool: &PgPool,
    actor: Option<Uuid>,
    action: &str,
    target_kind: &str,
    at: &str,
) {
    sqlx::query(
        "INSERT INTO admin_audit (actor_user_id, action, target_kind, target_id, payload, at) \
         VALUES ($1, $2, $3, NULL, $4, $5::timestamptz)",
    )
    .bind(actor)
    .bind(action)
    .bind(target_kind)
    .bind(json!({ "k": action }))
    .bind(at)
    .execute(pool)
    .await
    .unwrap();
}

#[sqlx::test(migrations = "./migrations")]
async fn list_joins_username_and_orders_newest_first(pool: PgPool) {
    let h = harness(pool.clone());
    let (admin_id, _cookie) = seed_admin(&pool, &h.app).await;
    insert_audit(&pool, Some(admin_id), "crawler_run", "crawler", "2026-06-01T00:00:00Z").await;
    insert_audit(&pool, None, "manga_resync", "manga", "2026-06-02T00:00:00Z").await;

    let (items, total) = admin_audit::list(&pool, AuditFilter::default(), 50, 0)
        .await
        .unwrap();
    assert_eq!(total, 2);
    // Newest first.
    assert_eq!(items[0].action, "manga_resync");
    assert_eq!(items[1].action, "crawler_run");
    // Username join: the admin actor resolves; the NULL actor stays None.
    assert_eq!(items[1].actor_user_id, Some(admin_id));
    assert!(items[1].actor_username.is_some());
    assert_eq!(items[0].actor_user_id, None);
    assert_eq!(items[0].actor_username, None);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_filters_by_action_target_kind_actor_and_since(pool: PgPool) {
    let h = harness(pool.clone());
    let (admin_id, _cookie) = seed_admin(&pool, &h.app).await;
    insert_audit(&pool, Some(admin_id), "crawler_run", "crawler", "2026-05-01T00:00:00Z").await;
    insert_audit(&pool, Some(admin_id), "manga_resync", "manga", "2026-06-10T00:00:00Z").await;
    insert_audit(&pool, None, "crawler_run", "crawler", "2026-06-11T00:00:00Z").await;

    // Filter by action.
    let (items, total) = admin_audit::list(
        &pool,
        AuditFilter { action: Some("crawler_run"), ..Default::default() },
        50,
        0,
    )
    .await
    .unwrap();
    assert_eq!(total, 2);
    assert!(items.iter().all(|r| r.action == "crawler_run"));

    // Filter by target_kind.
    let (_items, total) = admin_audit::list(
        &pool,
        AuditFilter { target_kind: Some("manga"), ..Default::default() },
        50,
        0,
    )
    .await
    .unwrap();
    assert_eq!(total, 1);

    // Filter by actor.
    let (_items, total) = admin_audit::list(
        &pool,
        AuditFilter { actor_user_id: Some(admin_id), ..Default::default() },
        50,
        0,
    )
    .await
    .unwrap();
    assert_eq!(total, 2);

    // Filter by since (only the two June rows).
    let since = chrono::DateTime::parse_from_rfc3339("2026-06-01T00:00:00Z")
        .unwrap()
        .with_timezone(&chrono::Utc);
    let (_items, total) = admin_audit::list(
        &pool,
        AuditFilter { since: Some(since), ..Default::default() },
        50,
        0,
    )
    .await
    .unwrap();
    assert_eq!(total, 2);
}

#[sqlx::test(migrations = "./migrations")]
async fn endpoint_requires_admin(pool: PgPool) {
    let h = harness(pool.clone());
    let (_u, cookie) = register_user(&h.app).await;
    let resp = h
        .app
        .oneshot(get_with_cookie("/api/v1/admin/audit", &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

#[sqlx::test(migrations = "./migrations")]
async fn endpoint_returns_paged_shape_and_honors_filter(pool: PgPool) {
    let h = harness(pool.clone());
    let (admin_id, cookie) = seed_admin(&pool, &h.app).await;
    insert_audit(&pool, Some(admin_id), "crawler_run", "crawler", "2026-06-01T00:00:00Z").await;
    insert_audit(&pool, Some(admin_id), "manga_resync", "manga", "2026-06-02T00:00:00Z").await;

    // Unfiltered: both rows + paged envelope.
    let resp = h
        .app
        .clone()
        .oneshot(get_with_cookie("/api/v1/admin/audit", &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_json(resp).await;
    assert_eq!(body["page"]["total"], 2);
    assert_eq!(body["items"].as_array().unwrap().len(), 2);
    assert!(body["items"][0]["actor_username"].is_string());

    // Filter by action.
    let resp = h
        .app
        .clone()
        .oneshot(get_with_cookie(
            "/api/v1/admin/audit?action=manga_resync",
            &cookie,
        ))
        .await
        .unwrap();
    let body = body_json(resp).await;
    assert_eq!(body["page"]["total"], 1);
    assert_eq!(body["items"][0]["action"], "manga_resync");
    assert_eq!(body["items"][0]["target_kind"], "manga");
}
