//! Integration tests for source registry handlers.

use axum::{
    body::Body,
    http::{Method, Request, StatusCode},
    routing::{get, patch, post},
    Router,
};
use serde_json::json;
use stemedb_storage::HybridStore;
use stemedb_wal::Journal;
use tower::ServiceExt;

use crate::{
    dto::{
        ListSourcesResponse, QuarantineSourceResponse, RestoreSourceResponse, SourceImpactResponse,
        SourceRecordDto,
    },
    state::AppState,
};

use super::handlers::{
    export_source_impact, get_source, get_source_impact, list_sources, quarantine_source,
    register_source, restore_source, update_source_status,
};

/// Test context that keeps temp dir alive.
struct TestContext {
    app: Router,
    #[allow(dead_code)]
    temp_dir: tempfile::TempDir,
}

/// Create a test app with in-memory storage.
async fn test_app() -> TestContext {
    let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
    let wal_path = temp_dir.path().join("wal");
    let store_path = temp_dir.path().join("store");

    let write_journal = Journal::open(&wal_path).expect("failed to open write journal");
    let read_journal = Journal::open(&wal_path).expect("failed to open read journal");
    let store = std::sync::Arc::new(HybridStore::open(&store_path).expect("failed to open store"));

    let state = AppState::new(write_journal, read_journal, store);

    let app = Router::new()
        .route("/v1/sources", post(register_source))
        .route("/v1/sources", get(list_sources))
        .route("/v1/sources/:hash", get(get_source))
        .route("/v1/sources/:hash/status", patch(update_source_status))
        .route("/v1/sources/:hash/impact", get(get_source_impact))
        .route("/v1/sources/:hash/quarantine", post(quarantine_source))
        .route("/v1/sources/:hash/restore", post(restore_source))
        .route("/v1/sources/:hash/impact/export", get(export_source_impact))
        .with_state(state);

    TestContext { app, temp_dir }
}

#[tokio::test]
async fn test_register_and_get_source() {
    let ctx = test_app().await;

    // Register a source
    let hash = "a".repeat(64);
    let req_body = json!({
        "hash": hash,
        "label": "RFC 7519 - JSON Web Token",
        "url": "https://tools.ietf.org/html/rfc7519",
        "tier": 0
    });

    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::CREATED);

    // Get the source
    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let source: SourceRecordDto = serde_json::from_slice(&body).expect("deserialize");

    assert_eq!(source.label, "RFC 7519 - JSON Web Token");
    assert_eq!(source.tier, 0);
    assert_eq!(source.tier_label, "Regulatory");
    assert_eq!(source.status, "active");
}

#[tokio::test]
async fn test_update_status() {
    let ctx = test_app().await;

    // Register a source
    let hash = "b".repeat(64);
    let req_body = json!({
        "hash": hash,
        "label": "Test Source",
        "tier": 1
    });

    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Update status to deprecated
    let update_body = json!({
        "status": "deprecated"
    });

    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::PATCH)
                .uri(format!("/v1/sources/{}/status", hash))
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&update_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    // Verify the status changed
    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let source: SourceRecordDto = serde_json::from_slice(&body).expect("deserialize");

    assert_eq!(source.status, "deprecated");
}

#[tokio::test]
async fn test_list_sources() {
    let ctx = test_app().await;

    // Register multiple sources
    for (i, tier) in [(0, 0), (1, 1), (2, 0)].iter() {
        let hash = "c".repeat(63) + &i.to_string();
        let req_body = json!({
            "hash": hash,
            "label": format!("Source {}", i),
            "tier": tier
        });

        ctx.app
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/v1/sources")
                    .header("content-type", "application/json")
                    .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                    .expect("request"),
            )
            .await
            .expect("response");
    }

    // List all sources
    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri("/v1/sources")
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let list: ListSourcesResponse = serde_json::from_slice(&body).expect("deserialize");

    assert_eq!(list.count, 3);

    // Filter by tier
    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri("/v1/sources?tier=0")
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let list: ListSourcesResponse = serde_json::from_slice(&body).expect("deserialize");

    assert_eq!(list.count, 2); // Sources 0 and 2 are tier 0
}

#[tokio::test]
async fn test_get_nonexistent_source() {
    let ctx = test_app().await;

    let hash = "d".repeat(64);
    let response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn test_invalid_hash_format() {
    let ctx = test_app().await;

    // Too short
    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri("/v1/sources/abc123")
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    // Non-hex
    let response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}", "g".repeat(64)))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

// ============================================================================
// Impact Analysis Tests (P3.1)
// ============================================================================

#[tokio::test]
async fn test_get_impact_not_found() {
    let ctx = test_app().await;

    // Get impact for non-existent source
    let hash = "e".repeat(64);
    let response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}/impact", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    // Should return 200 with unknown status and 0 assertions
    assert_eq!(response.status(), StatusCode::OK);

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let impact: SourceImpactResponse = serde_json::from_slice(&body).expect("deserialize");

    assert_eq!(impact.status, "unknown");
    assert_eq!(impact.assertion_count, 0);
    assert!(impact.label.is_none());
}

#[tokio::test]
async fn test_get_impact_for_registered_source() {
    let ctx = test_app().await;

    // Register a source
    let hash = "f".repeat(64);
    let req_body = json!({
        "hash": hash,
        "label": "Test Source for Impact",
        "tier": 2
    });

    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Get impact
    let response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}/impact", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let impact: SourceImpactResponse = serde_json::from_slice(&body).expect("deserialize");

    assert_eq!(impact.source_hash, hash);
    assert_eq!(impact.label, Some("Test Source for Impact".to_string()));
    assert_eq!(impact.status, "active");
    assert_eq!(impact.assertion_count, 0); // No assertions cite this source yet
}

#[tokio::test]
async fn test_quarantine_preview_mode() {
    let ctx = test_app().await;

    // Register a source
    let hash = "0102030405060708091011121314151617181920212223242526272829303132".to_string();
    let req_body = json!({
        "hash": hash,
        "label": "Source to Preview Quarantine",
        "tier": 1
    });

    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Quarantine with preview=true
    let quarantine_body = json!({
        "preview": true,
        "reason": "Testing preview mode"
    });

    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri(format!("/v1/sources/{}/quarantine", hash))
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&quarantine_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let resp: QuarantineSourceResponse = serde_json::from_slice(&body).expect("deserialize");

    assert!(resp.preview);
    assert_eq!(resp.status, "active"); // Status should NOT change in preview mode
    assert!(resp.message.contains("Preview"));

    // Verify status did not actually change
    let check_response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    let body = axum::body::to_bytes(check_response.into_body(), usize::MAX).await.expect("body");
    let source: SourceRecordDto = serde_json::from_slice(&body).expect("deserialize");
    assert_eq!(source.status, "active");
}

#[tokio::test]
async fn test_quarantine_commit_mode() {
    let ctx = test_app().await;

    // Register a source
    let hash = "112233445566778899001122334455667788990011223344556677889900aabb".to_string();
    let req_body = json!({
        "hash": hash,
        "label": "Source to Actually Quarantine",
        "tier": 1
    });

    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Quarantine with preview=false (commit)
    let quarantine_body = json!({
        "preview": false,
        "reason": "Study was retracted"
    });

    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri(format!("/v1/sources/{}/quarantine", hash))
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&quarantine_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let resp: QuarantineSourceResponse = serde_json::from_slice(&body).expect("deserialize");

    assert!(!resp.preview);
    assert_eq!(resp.status, "quarantined");

    // Verify status actually changed
    let check_response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    let body = axum::body::to_bytes(check_response.into_body(), usize::MAX).await.expect("body");
    let source: SourceRecordDto = serde_json::from_slice(&body).expect("deserialize");
    assert_eq!(source.status, "quarantined");
}

#[tokio::test]
async fn test_restore_quarantined_source() {
    let ctx = test_app().await;

    // Register and quarantine a source
    let hash = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899".to_string();
    let req_body = json!({
        "hash": hash,
        "label": "Source to Restore",
        "tier": 2
    });

    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Quarantine it
    let quarantine_body = json!({ "preview": false });
    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri(format!("/v1/sources/{}/quarantine", hash))
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&quarantine_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Restore it
    let restore_body = json!({
        "reason": "Retraction was reversed"
    });

    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri(format!("/v1/sources/{}/restore", hash))
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&restore_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let resp: RestoreSourceResponse = serde_json::from_slice(&body).expect("deserialize");

    assert_eq!(resp.status, "active");

    // Verify status is active
    let check_response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    let body = axum::body::to_bytes(check_response.into_body(), usize::MAX).await.expect("body");
    let source: SourceRecordDto = serde_json::from_slice(&body).expect("deserialize");
    assert_eq!(source.status, "active");
}

#[tokio::test]
async fn test_restore_non_quarantined_fails() {
    let ctx = test_app().await;

    // Register an active source
    let hash = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string();
    let req_body = json!({
        "hash": hash,
        "label": "Active Source",
        "tier": 0
    });

    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Try to restore (should fail - not quarantined)
    let restore_body = json!({});

    let response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri(format!("/v1/sources/{}/restore", hash))
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&restore_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::CONFLICT);
}

// ============================================================================
// P3.2 Cascade Flagging Tests
// ============================================================================

#[tokio::test]
async fn test_export_json_format() {
    let ctx = test_app().await;

    // Register a source
    let hash = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string();
    let req_body = json!({
        "hash": hash,
        "label": "Test Source for Export",
        "tier": 1
    });

    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Export as JSON (no assertions citing this source, so expect empty array)
    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}/impact/export?format=json", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let rows: Vec<crate::dto::ImpactExportRow> =
        serde_json::from_slice(&body).expect("deserialize");

    // No assertions cite this source yet
    assert!(rows.is_empty());
}

#[tokio::test]
async fn test_export_csv_format() {
    let ctx = test_app().await;

    // Register a source
    let hash = "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string();
    let req_body = json!({
        "hash": hash,
        "label": "Test Source for CSV Export",
        "tier": 2
    });

    ctx.app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/v1/sources")
                .header("content-type", "application/json")
                .body(Body::from(serde_json::to_string(&req_body).expect("json")))
                .expect("request"),
        )
        .await
        .expect("response");

    // Export as CSV
    let response = ctx
        .app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri(format!("/v1/sources/{}/impact/export?format=csv", hash))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::OK);

    // Check content type
    let content_type = response.headers().get("content-type").expect("content-type header");
    assert!(content_type.to_str().expect("str").contains("text/csv"));

    // Check content disposition
    let content_disposition =
        response.headers().get("content-disposition").expect("content-disposition header");
    assert!(content_disposition.to_str().expect("str").contains("attachment"));
    assert!(content_disposition.to_str().expect("str").contains(".csv"));

    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("body");
    let csv = String::from_utf8(body.to_vec()).expect("utf8");

    // Check header row
    assert!(csv
        .starts_with("assertion_hash,subject,predicate,agent_id,timestamp,lifecycle,confidence\n"));
}

#[tokio::test]
async fn test_export_invalid_hash() {
    let ctx = test_app().await;

    // Export with invalid hash
    let response = ctx
        .app
        .oneshot(
            Request::builder()
                .method(Method::GET)
                .uri("/v1/sources/invalid_hash/impact/export")
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("response");

    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
