use axum::{
    extract::{Query, State},
    http::StatusCode,
    routing::get,
    Json, Router,
};

use crate::types::*;
use crate::AppState;

pub(crate) fn router() -> Router<AppState> {
    Router::new().route("/api/v1/search", get(global_search))
}

#[derive(serde::Deserialize)]
pub(crate) struct SearchQuery {
    q: String,
}

async fn global_search(
    State(state): State<AppState>,
    Query(params): Query<SearchQuery>,
) -> Result<Json<ApiResponse<Vec<SearchResult>>>, StatusCode> {
    if params.q.trim().is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }
    let results = state
        .db
        .global_search(&params.q)
        .map_err(|err| crate::db_error_status("search.global_search", "search", err))?;
    Ok(Json(ApiResponse::ok(results)))
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::middleware;
    use chrono::Utc;
    use tokio::sync::broadcast;
    use uuid::Uuid;

    async fn spawn_app() -> String {
        let db_path =
            std::env::temp_dir().join(format!("theseus-search-test-{}.db", Uuid::new_v4()));
        let db = Box::leak(Box::new(crate::db::Database::open(&db_path).unwrap()));
        let project = db
            .create_project(&CreateProjectRequest {
                name: "Search".into(),
            })
            .unwrap();
        db.insert_node(&Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "Nacos SSRF".into(),
            node_type: NodeType::Vulnerability,
            position: None,
            properties: serde_json::json!({"content":"gopher payload"}),
            created_at: Utc::now(),
        })
        .unwrap();
        let (tx, _) = broadcast::channel(16);
        let state = crate::AppState {
            db,
            tx,
            config: crate::Config {
                            api_key: None,
                            deepseek_api_key: None,
                            cors_origins: None,
                providers: std::collections::HashMap::new(),
                agent_workers: Vec::new(),
                default_provider: None,
                        },
            api_key: None,
            http_client: reqwest::Client::new(),
                provider_registry: None,
        };
        let app = router()
            .layer(middleware::from_fn(crate::json_error_middleware))
            .with_state(state);
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
        format!("http://{addr}")
    }

    async fn json(resp: reqwest::Response) -> serde_json::Value {
        resp.json::<serde_json::Value>().await.unwrap()
    }

    #[tokio::test]
    async fn global_search_requires_query_and_wraps_results() {
        let base = spawn_app().await;
        let client = reqwest::Client::new();

        let found = client
            .get(format!("{base}/api/v1/search?q=nacos"))
            .send()
            .await
            .unwrap();
        assert_eq!(found.status(), StatusCode::OK);
        let body = json(found).await;
        assert_eq!(body["success"], true);
        assert_eq!(body["data"].as_array().unwrap().len(), 1);
        assert_eq!(body["data"][0]["node_label"], "Nacos SSRF");

        let empty = client
            .get(format!("{base}/api/v1/search?q=   "))
            .send()
            .await
            .unwrap();
        assert_eq!(empty.status(), StatusCode::BAD_REQUEST);
        assert_eq!(json(empty).await["success"], false);
    }
}
