use axum::{
    body::Body,
    http::{Request, StatusCode},
    routing::get,
    Router,
};
use http_body_util::BodyExt;
use std::{path::PathBuf, sync::Arc};
use tempfile::TempDir;
use tokio::sync::broadcast;
use tower::ServiceExt;

use pumlv::routes::{
    events_handler, files_handler, index_handler, render_handler, AppState,
};

// ── Test helpers ──────────────────────────────────────────────────────────────

fn make_state(root: Arc<PathBuf>) -> AppState {
    let (tx, _) = broadcast::channel(8);
    AppState {
        root,
        plantuml_jar: Arc::new(PathBuf::from("/nonexistent/plantuml.jar")),
        event_tx: tx,
    }
}

fn make_tree() -> TempDir {
    let dir = TempDir::new().unwrap();
    std::fs::write(dir.path().join("hello.puml"), "@startuml\nA -> B\n@enduml").unwrap();
    std::fs::create_dir(dir.path().join("sub")).unwrap();
    std::fs::write(
        dir.path().join("sub/world.puml"),
        "@startuml\nC -> D\n@enduml",
    )
    .unwrap();
    dir
}

fn test_router(state: AppState) -> Router {
    Router::new()
        .route("/", get(index_handler))
        .route("/api/files", get(files_handler))
        .route("/api/render", get(render_handler))
        .route("/api/events", get(events_handler))
        .with_state(state)
}

async fn body_string(body: Body) -> String {
    let bytes = body.collect().await.unwrap().to_bytes();
    String::from_utf8(bytes.to_vec()).unwrap()
}

// ── Index ─────────────────────────────────────────────────────────────────────

#[tokio::test]
async fn index_returns_html() {
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_string(resp.into_body()).await;
    assert!(body.contains("pumlv"));
    assert!(body.contains("<html"));
}

// ── Files endpoint ────────────────────────────────────────────────────────────

#[tokio::test]
async fn files_lists_puml_files() {
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/files")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_string(resp.into_body()).await;
    assert!(body.contains("hello.puml"));
    assert!(body.contains("sub/world.puml"));
}

#[tokio::test]
async fn files_response_contains_data_path_attributes() {
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/files")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    let body = body_string(resp.into_body()).await;
    assert!(body.contains(r#"data-path="hello.puml""#));
}

// ── Render endpoint ───────────────────────────────────────────────────────────

#[tokio::test]
async fn render_rejects_path_traversal() {
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/render?f=../etc/passwd")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

#[tokio::test]
async fn render_rejects_absolute_path() {
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/render?f=%2Fetc%2Fpasswd")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    // /etc/passwd has no .puml extension → 400 Bad Request
    assert!(
        resp.status() == StatusCode::BAD_REQUEST
            || resp.status() == StatusCode::FORBIDDEN
    );
}

#[tokio::test]
async fn render_returns_bad_request_for_nonexistent_file() {
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/render?f=ghost.puml")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

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

#[tokio::test]
async fn render_returns_bad_request_for_wrong_extension() {
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/render?f=notes.txt")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

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

#[tokio::test]
async fn render_returns_server_error_when_plantuml_missing() {
    // File exists but plantuml.jar does not → 500.
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/render?f=hello.puml")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}

// ── Events endpoint ───────────────────────────────────────────────────────────

#[tokio::test]
async fn events_returns_sse_content_type() {
    let dir  = make_tree();
    let app  = test_router(make_state(Arc::new(dir.path().to_path_buf())));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/events")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(resp.status(), StatusCode::OK);
    let ct = resp
        .headers()
        .get("content-type")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");
    assert!(ct.contains("text/event-stream"), "expected SSE content-type, got: {ct}");
}
