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

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

pub(crate) fn router() -> Router<AppState> {
    Router::new().route("/api/v1/projects/:project_id/timeline", get(get_timeline))
}

async fn get_timeline(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
) -> Result<Json<ApiResponse<Vec<TimelineItem>>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let timeline = state
        .db
        .project_timeline(pid)
        .map_err(|err| crate::db_error_status("timeline.project_timeline", "timeline", err))?;
    Ok(Json(ApiResponse::ok(timeline)))
}

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

    async fn spawn_app() -> (String, Uuid) {
        let db_path =
            std::env::temp_dir().join(format!("theseus-timeline-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: "Timeline".into(),
            })
            .unwrap();
        let first = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "Entry".into(),
            node_type: NodeType::EntryPoint,
            position: None,
            properties: serde_json::json!({}),
            created_at: Utc::now(),
        };
        let second = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "Exploit".into(),
            node_type: NodeType::AttackStep,
            position: None,
            properties: serde_json::json!({}),
            created_at: Utc::now(),
        };
        db.insert_node(&first).unwrap();
        db.insert_node(&second).unwrap();
        db.insert_edge(&Edge {
            id: Uuid::new_v4(),
            project_id: project.id,
            source_node_id: first.id,
            target_node_id: second.id,
            label: Some("chains".into()),
            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}"), project.id)
    }

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

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

        let timeline = client
            .get(format!("{base}/api/v1/projects/{pid}/timeline"))
            .send()
            .await
            .unwrap();
        assert_eq!(timeline.status(), StatusCode::OK);
        let body = json(timeline).await;
        assert_eq!(body["success"], true);
        assert_eq!(body["data"].as_array().unwrap().len(), 2);
        assert!(body["data"][0]["outgoing"].is_array());
        assert!(body["data"][1]["incoming"].is_array());

        let missing = client
            .get(format!("{base}/api/v1/projects/abcdef/timeline"))
            .send()
            .await
            .unwrap();
        assert_eq!(missing.status(), StatusCode::NOT_FOUND);
    }
}
