use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;

use crate::helpers::{
    MINIMAL_DOT, api, create_and_start_run_from_manifest, minimal_manifest_json_with_dry_run,
    response_json, response_status, test_app_state_with_options, test_app_with_scheduler,
    test_settings, wait_for_run_status,
};

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn archived_runs_reject_mutations_with_actionable_body() {
    let state = test_app_state_with_options(test_settings(), 5);
    let app = test_app_with_scheduler(state);

    let run_id =
        create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT))
            .await;
    let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await;
    assert_eq!(status, "succeeded");

    let req = Request::builder()
        .method("POST")
        .uri(api(&format!("/runs/{run_id}/archive")))
        .body(Body::empty())
        .unwrap();
    let response = app.clone().oneshot(req).await.unwrap();
    let body = response_json(
        response,
        StatusCode::OK,
        format!("POST /api/v1/runs/{run_id}/archive"),
    )
    .await;
    assert_eq!(body["lifecycle"]["status"]["kind"], "succeeded");
    assert_eq!(body["lifecycle"]["archived"], true);

    for path in &["/cancel", "/pause", "/unpause", "/start"] {
        let req = Request::builder()
            .method("POST")
            .uri(api(&format!("/runs/{run_id}{path}")))
            .body(Body::empty())
            .unwrap();
        let response = app.clone().oneshot(req).await.unwrap();
        let body = response_json(
            response,
            StatusCode::CONFLICT,
            format!("POST /api/v1/runs/{run_id}{path}"),
        )
        .await;
        let detail = body["errors"][0]["detail"].as_str().unwrap_or_default();
        assert!(
            detail.contains("is archived") && detail.contains("fabro unarchive"),
            "expected archived-rejection body on {path}, got: {body}"
        );
    }

    let req = Request::builder()
        .method("POST")
        .uri(api(&format!("/runs/{run_id}/events")))
        .header("content-type", "application/json")
        .body(Body::from(
            serde_json::to_string(&serde_json::json!({
                "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
                "ts": "2026-04-19T12:00:00.000Z",
                "run_id": run_id,
                "event": "agent.message",
                "properties": {}
            }))
            .unwrap(),
        ))
        .unwrap();
    let response = app.clone().oneshot(req).await.unwrap();
    let body = response_json(
        response,
        StatusCode::CONFLICT,
        format!("POST /api/v1/runs/{run_id}/events"),
    )
    .await;
    let detail = body["errors"][0]["detail"].as_str().unwrap_or_default();
    assert!(
        detail.contains("is archived") && detail.contains("fabro unarchive"),
        "expected archived-rejection body on /events, got: {body}"
    );

    // The archive guard runs before each endpoint's state-specific lookups, so
    // synthetic stage/question/filename values are enough to drive these
    // write surfaces into the guard.
    for (method, path, body, content_type) in [
        (
            "POST",
            format!("/runs/{run_id}/questions/q-fake/answer"),
            r#"{"kind":"text","text":"x"}"#,
            "application/json",
        ),
        (
            "POST",
            format!("/runs/{run_id}/stages/fake@1/artifacts?filename=smoke.txt&retry=1"),
            "payload",
            "application/octet-stream",
        ),
        (
            "PUT",
            format!("/runs/{run_id}/sandbox/file?path=smoke.txt"),
            "payload",
            "application/octet-stream",
        ),
        (
            "POST",
            format!("/runs/{run_id}/blobs"),
            "payload",
            "application/octet-stream",
        ),
    ] {
        let req = Request::builder()
            .method(method)
            .uri(api(&path))
            .header("content-type", content_type)
            .body(Body::from(body))
            .unwrap();
        let response = app.clone().oneshot(req).await.unwrap();
        let body = response_json(
            response,
            StatusCode::CONFLICT,
            format!("{method} /api/v1{path}"),
        )
        .await;
        let detail = body["errors"][0]["detail"].as_str().unwrap_or_default();
        assert!(
            detail.contains("is archived") && detail.contains("fabro unarchive"),
            "expected archived-rejection body on {method} {path}, got: {body}"
        );
    }

    // Unarchive restores the prior terminal status.
    let req = Request::builder()
        .method("POST")
        .uri(api(&format!("/runs/{run_id}/unarchive")))
        .body(Body::empty())
        .unwrap();
    let response = app.clone().oneshot(req).await.unwrap();
    let body = response_json(
        response,
        StatusCode::OK,
        format!("POST /api/v1/runs/{run_id}/unarchive"),
    )
    .await;
    assert_eq!(body["lifecycle"]["status"]["kind"], "succeeded");
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn appending_run_archived_event_directly_is_rejected() {
    // Regression: archive/unarchive events must not be injectable via
    // `append_run_event` — clients must use the operation endpoints.
    let state = test_app_state_with_options(test_settings(), 5);
    let app = test_app_with_scheduler(state);

    let run_id =
        create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT))
            .await;
    wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await;

    let req = Request::builder()
        .method("POST")
        .uri(api(&format!("/runs/{run_id}/events")))
        .header("content-type", "application/json")
        .body(Body::from(
            serde_json::to_string(&serde_json::json!({
                "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
                "ts": "2026-04-19T12:00:00.000Z",
                "run_id": run_id,
                "event": "run.archived",
                "properties": {}
            }))
            .unwrap(),
        ))
        .unwrap();
    let response = app.clone().oneshot(req).await.unwrap();
    let body = crate::helpers::response_json(
        response,
        StatusCode::BAD_REQUEST,
        format!("{}:{}", file!(), line!()),
    )
    .await;
    let detail = body["errors"][0]["detail"].as_str().unwrap_or_default();
    assert!(
        detail.contains("lifecycle event"),
        "expected lifecycle rejection, got: {body}"
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn archive_returns_404_for_unknown_run() {
    let state = test_app_state_with_options(test_settings(), 5);
    let app = test_app_with_scheduler(state);

    let req = Request::builder()
        .method("POST")
        .uri(api("/runs/01ARZ3NDEKTSV4RRFFQ69G5FAV/archive"))
        .body(Body::empty())
        .unwrap();
    let response = app.clone().oneshot(req).await.unwrap();
    response_status(
        response,
        StatusCode::NOT_FOUND,
        "POST /api/v1/runs/01ARZ3NDEKTSV4RRFFQ69G5FAV/archive",
    )
    .await;

    let req = Request::builder()
        .method("POST")
        .uri(api("/runs/01ARZ3NDEKTSV4RRFFQ69G5FAV/unarchive"))
        .body(Body::empty())
        .unwrap();
    let response = app.clone().oneshot(req).await.unwrap();
    response_status(
        response,
        StatusCode::NOT_FOUND,
        "POST /api/v1/runs/01ARZ3NDEKTSV4RRFFQ69G5FAV/unarchive",
    )
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_runs_respects_include_archived_flag() {
    let state = test_app_state_with_options(test_settings(), 5);
    let app = test_app_with_scheduler(state);

    let run_id =
        create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT))
            .await;
    wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await;

    // Archive it.
    let req = Request::builder()
        .method("POST")
        .uri(api(&format!("/runs/{run_id}/archive")))
        .body(Body::empty())
        .unwrap();
    response_status(
        app.clone().oneshot(req).await.unwrap(),
        StatusCode::OK,
        format!("POST /api/v1/runs/{run_id}/archive"),
    )
    .await;

    // Default listing hides archived.
    let req = Request::builder()
        .method("GET")
        .uri(api("/runs"))
        .body(Body::empty())
        .unwrap();
    let response = app.clone().oneshot(req).await.unwrap();
    let body = response_json(response, StatusCode::OK, "GET /api/v1/runs").await;
    let ids_visible: Vec<String> = body["data"]
        .as_array()
        .unwrap()
        .iter()
        .map(|item| item["id"].as_str().unwrap().to_string())
        .collect();
    assert!(
        !ids_visible.contains(&run_id),
        "archived run should be hidden, got {ids_visible:?}"
    );

    // `include_archived=true` surfaces it.
    let req = Request::builder()
        .method("GET")
        .uri(api("/runs?include_archived=true"))
        .body(Body::empty())
        .unwrap();
    let response = app.clone().oneshot(req).await.unwrap();
    let body = response_json(
        response,
        StatusCode::OK,
        "GET /api/v1/runs?include_archived=true",
    )
    .await;
    let ids_all: Vec<String> = body["data"]
        .as_array()
        .unwrap()
        .iter()
        .map(|item| item["id"].as_str().unwrap().to_string())
        .collect();
    assert!(
        ids_all.contains(&run_id),
        "include_archived=true should surface the run, got {ids_all:?}"
    );
}
