use axum::body::Body;
use axum::http::{Request, StatusCode};
use fabro_model::{Catalog, ProviderId};
use fabro_types::settings::run::EnvironmentProvider;
use tower::ServiceExt;

use crate::helpers::{
    MINIMAL_DOT, api, body_json, minimal_manifest_json, response_json, response_status,
    settings_from_toml, test_app_state_with_options,
};

async fn create_run(app: &axum::Router, mut manifest: serde_json::Value) -> serde_json::Value {
    manifest
        .as_object_mut()
        .expect("manifest should be an object")
        .entry("configs")
        .or_insert_with(|| serde_json::json!([]));
    let request = Request::builder()
        .method("POST")
        .uri(api("/runs"))
        .header("content-type", "application/json")
        .body(Body::from(
            serde_json::to_string(&manifest).expect("manifest should serialize"),
        ))
        .expect("create run request should build");
    response_json(
        app.clone().oneshot(request).await.unwrap(),
        StatusCode::CREATED,
        "POST /api/v1/runs",
    )
    .await
}

async fn request_json(
    app: &axum::Router,
    method: &str,
    path: String,
    body: serde_json::Value,
    expected: StatusCode,
) -> serde_json::Value {
    let request = Request::builder()
        .method(method)
        .uri(api(&path))
        .header("content-type", "application/json")
        .body(Body::from(
            serde_json::to_string(&body).expect("request body should serialize"),
        ))
        .expect("JSON request should build");
    response_json(
        app.clone().oneshot(request).await.unwrap(),
        expected,
        format!("{method} /api/v1{path}"),
    )
    .await
}

fn daytona_manifest() -> serde_json::Value {
    let mut manifest = minimal_manifest_json(MINIMAL_DOT);
    manifest["args"] = serde_json::json!({ "environment": "default" });
    manifest
}

fn daytona_disabled_settings() -> crate::helpers::TestAppSettings {
    settings_from_toml(
        r"
_version = 1

[server.sandbox.providers.daytona]
enabled = false
",
    )
}

fn daytona_disabled_app() -> (axum::Router, tempfile::TempDir) {
    let temp_dir = tempfile::tempdir().expect("daytona disabled test tempdir should be created");
    let active_config_path = temp_dir.path().join("settings.toml");
    let settings = daytona_disabled_settings();
    let state = fabro_server::test_support::TestAppStateBuilder::new()
        .runtime_settings(settings.server_settings, settings.manifest_run_defaults)
        .active_config_path(active_config_path)
        .default_environment_provider(Some(EnvironmentProvider::Daytona))
        .build();
    (
        fabro_server::test_support::build_test_router(state),
        temp_dir,
    )
}

#[tokio::test]
async fn create_run_rejects_disabled_sandbox_provider() {
    let (app, _temp_dir) = daytona_disabled_app();

    let request = Request::builder()
        .method("POST")
        .uri(api("/runs"))
        .header("content-type", "application/json")
        .body(Body::from(daytona_manifest().to_string()))
        .expect("create run request should build");
    let body = response_json(
        app.clone().oneshot(request).await.unwrap(),
        StatusCode::BAD_REQUEST,
        "POST /api/v1/runs",
    )
    .await;

    assert_eq!(
        body["errors"][0]["detail"],
        "sandbox provider \"daytona\" is disabled by server.sandbox.providers.daytona.enabled"
    );
}

#[tokio::test]
async fn preflight_reports_disabled_sandbox_provider() {
    let (app, _temp_dir) = daytona_disabled_app();

    let request = Request::builder()
        .method("POST")
        .uri(api("/preflight"))
        .header("content-type", "application/json")
        .body(Body::from(daytona_manifest().to_string()))
        .expect("preflight request should build");
    let body = response_json(
        app.clone().oneshot(request).await.unwrap(),
        StatusCode::OK,
        "POST /api/v1/preflight",
    )
    .await;

    assert_eq!(body["ok"], false);
    let checks = body["checks"]["sections"][0]["checks"]
        .as_array()
        .expect("preflight checks should be an array");
    let policy_check = checks
        .iter()
        .find(|check| check["name"] == "Sandbox Provider Policy")
        .expect("policy check should be present");
    assert_eq!(policy_check["status"], "error");
    assert_eq!(
        policy_check["summary"],
        "sandbox provider \"daytona\" is disabled by server.sandbox.providers.daytona.enabled"
    );
}

#[tokio::test]
async fn run_responses_include_ask_fabro_affordance() {
    let settings = settings_from_toml(
        r"
_version = 1
",
    );
    let state = fabro_server::test_support::TestAppStateBuilder::new()
        .runtime_settings(settings.server_settings, settings.manifest_run_defaults)
        .vault_entries([("OPENAI_API_KEY", "test-key")])
        .build();
    let app = fabro_server::test_support::build_test_router(state);
    let created = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
    let run_id = created["id"].as_str().unwrap();

    assert_eq!(created["ask_fabro"]["available"], false);
    assert_eq!(
        created["ask_fabro"]["unavailable_reason"],
        "sandbox_not_ready"
    );
    let default_openai_model = Catalog::builtin()
        .default_for_provider(&ProviderId::openai())
        .expect("the built-in OpenAI provider should have a default model");
    assert_eq!(
        created["ask_fabro"]["default_model"].as_str(),
        Some(default_openai_model.id())
    );

    let get_request = Request::builder()
        .method("GET")
        .uri(api(&format!("/runs/{run_id}")))
        .body(Body::empty())
        .unwrap();
    let fetched = response_json(
        app.clone().oneshot(get_request).await.unwrap(),
        StatusCode::OK,
        format!("GET /api/v1/runs/{run_id}"),
    )
    .await;
    assert_eq!(fetched["ask_fabro"], created["ask_fabro"]);

    let list_request = Request::builder()
        .method("GET")
        .uri(api("/runs"))
        .body(Body::empty())
        .unwrap();
    let list = response_json(
        app.clone().oneshot(list_request).await.unwrap(),
        StatusCode::OK,
        "GET /api/v1/runs",
    )
    .await;
    assert_eq!(list["data"][0]["ask_fabro"], created["ask_fabro"]);
}

#[tokio::test]
async fn retrieve_run_settings_returns_dense_snapshot() {
    let storage_dir = tempfile::tempdir().unwrap();
    let settings = settings_from_toml(&format!(
        r#"
_version = 1

[server.listen]
type = "tcp"
address = "127.0.0.1:32276"

[server.auth]
methods = ["dev-token", "github"]

[server.auth.github]
allowed_usernames = ["alice"]

[server.storage]
root = "{}"

[server.scheduler]
max_concurrent_runs = 9

[server.integrations.github]
app_id = "{{{{ env.GITHUB_APP_ID }}}}"
client_id = "Iv1.github"
slug = "fabro-app"
"#,
        storage_dir.path().display()
    ));

    let app =
        fabro_server::test_support::build_test_router(test_app_state_with_options(settings, 5));
    let mut manifest = minimal_manifest_json(MINIMAL_DOT);
    manifest["configs"] = serde_json::json!([{
        "type": "user",
        "path": "/tmp/home/.fabro/settings.toml",
        "source": r#"
_version = 1

[run]
goal = "Ship it"

[cli.output]
verbosity = "verbose"
"#
    }]);

    let create_request = Request::builder()
        .method("POST")
        .uri(api("/runs"))
        .header("content-type", "application/json")
        .body(Body::from(serde_json::to_string(&manifest).unwrap()))
        .unwrap();
    let create_response = app.clone().oneshot(create_request).await.unwrap();
    let create_status = create_response.status();
    let create_body = body_json(create_response.into_body()).await;
    assert_eq!(create_status, StatusCode::CREATED, "{create_body}");
    let run_id = create_body["id"]
        .as_str()
        .expect("run ID should be present");

    let get_request = Request::builder()
        .method("GET")
        .uri(api(&format!("/runs/{run_id}/settings")))
        .body(Body::empty())
        .unwrap();
    let response = app.oneshot(get_request).await.unwrap();

    let body = response_json(
        response,
        StatusCode::OK,
        format!("GET /api/v1/runs/{run_id}/settings"),
    )
    .await;
    assert!(body["project"].get("directory").is_none());
    assert_eq!(body["workflow"]["graph"], "workflow.fabro");
    assert_eq!(body["run"]["goal"]["type"], "inline");
    assert_eq!(body["run"]["goal"]["value"], "Ship it");
    assert!(body.pointer("/_version").is_none());
    assert!(body.pointer("/cli").is_none());
    assert!(body.pointer("/features").is_none());
    assert!(body.pointer("/server").is_none());
}

#[tokio::test]
async fn create_run_can_set_parent_and_list_children() {
    let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state());
    let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
    let parent_id = parent["id"].as_str().unwrap();
    let mut child_manifest = minimal_manifest_json(MINIMAL_DOT);
    child_manifest["parent_id"] = serde_json::json!(parent_id);

    let child = create_run(&app, child_manifest).await;
    let child_id = child["id"].as_str().unwrap();

    assert_eq!(child["parent_id"], parent_id);
    let list_request = Request::builder()
        .method("GET")
        .uri(api(&format!("/runs?parent_id={parent_id}")))
        .body(Body::empty())
        .unwrap();
    let list = response_json(
        app.clone().oneshot(list_request).await.unwrap(),
        StatusCode::OK,
        "GET /api/v1/runs?parent_id",
    )
    .await;
    assert_eq!(list["data"].as_array().unwrap().len(), 1);
    assert_eq!(list["data"][0]["id"], child_id);
}

#[tokio::test]
async fn link_relink_and_unlink_parent_are_idempotent() {
    let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state());
    let parent_1 = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
    let parent_2 = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
    let child = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
    let parent_1_id = parent_1["id"].as_str().unwrap();
    let parent_2_id = parent_2["id"].as_str().unwrap();
    let child_id = child["id"].as_str().unwrap();

    let linked = request_json(
        &app,
        "PUT",
        format!("/runs/{child_id}/parent"),
        serde_json::json!({ "parent_id": parent_1_id }),
        StatusCode::OK,
    )
    .await;
    assert_eq!(linked["parent_id"], parent_1_id);

    let linked_again = request_json(
        &app,
        "PUT",
        format!("/runs/{child_id}/parent"),
        serde_json::json!({ "parent_id": parent_1_id }),
        StatusCode::OK,
    )
    .await;
    assert_eq!(linked_again["parent_id"], parent_1_id);

    let relinked = request_json(
        &app,
        "PUT",
        format!("/runs/{child_id}/parent"),
        serde_json::json!({ "parent_id": parent_2_id }),
        StatusCode::OK,
    )
    .await;
    assert_eq!(relinked["parent_id"], parent_2_id);

    let events_request = Request::builder()
        .method("GET")
        .uri(api(&format!("/runs/{child_id}/events")))
        .body(Body::empty())
        .unwrap();
    let events = response_json(
        app.clone().oneshot(events_request).await.unwrap(),
        StatusCode::OK,
        format!("GET /api/v1/runs/{child_id}/events"),
    )
    .await;
    let event_names = events["data"]
        .as_array()
        .unwrap()
        .iter()
        .map(|event| event["event"].as_str().unwrap())
        .collect::<Vec<_>>();
    assert_eq!(event_names, vec![
        "run.created",
        "run.submitted",
        "run.parent.linked",
        "run.parent.linked"
    ]);

    let unlink_request = Request::builder()
        .method("DELETE")
        .uri(api(&format!("/runs/{child_id}/parent")))
        .body(Body::empty())
        .unwrap();
    let unlinked = response_json(
        app.clone().oneshot(unlink_request).await.unwrap(),
        StatusCode::OK,
        format!("DELETE /api/v1/runs/{child_id}/parent"),
    )
    .await;
    assert!(unlinked["parent_id"].is_null());

    let unlink_again = Request::builder()
        .method("DELETE")
        .uri(api(&format!("/runs/{child_id}/parent")))
        .body(Body::empty())
        .unwrap();
    let unlinked_again = response_json(
        app.clone().oneshot(unlink_again).await.unwrap(),
        StatusCode::OK,
        format!("DELETE /api/v1/runs/{child_id}/parent"),
    )
    .await;
    assert!(unlinked_again["parent_id"].is_null());
}

#[tokio::test]
async fn deleting_parent_leaves_child_parent_id_as_historical_reference() {
    let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state());
    let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
    let parent_id = parent["id"].as_str().unwrap();
    let mut child_manifest = minimal_manifest_json(MINIMAL_DOT);
    child_manifest["parent_id"] = serde_json::json!(parent_id);
    let child = create_run(&app, child_manifest).await;
    let child_id = child["id"].as_str().unwrap();

    let delete_request = Request::builder()
        .method("DELETE")
        .uri(api(&format!("/runs/{parent_id}?force=true")))
        .body(Body::empty())
        .unwrap();
    crate::helpers::checked_response_in(
        app.clone().oneshot(delete_request).await.unwrap(),
        &[StatusCode::OK, StatusCode::NO_CONTENT],
        format!("DELETE /api/v1/runs/{parent_id}?force=true"),
    )
    .await;

    let get_child = Request::builder()
        .method("GET")
        .uri(api(&format!("/runs/{child_id}")))
        .body(Body::empty())
        .unwrap();
    let child_after_delete = response_json(
        app.clone().oneshot(get_child).await.unwrap(),
        StatusCode::OK,
        format!("GET /api/v1/runs/{child_id}"),
    )
    .await;
    assert_eq!(child_after_delete["parent_id"], parent_id);

    request_json(
        &app,
        "PUT",
        format!("/runs/{child_id}/parent"),
        serde_json::json!({ "parent_id": parent_id }),
        StatusCode::NOT_FOUND,
    )
    .await;
}

#[tokio::test]
async fn parent_link_validation_rejects_missing_self_and_cycles() {
    let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state());
    let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
    let child = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
    let parent_id = parent["id"].as_str().unwrap();
    let child_id = child["id"].as_str().unwrap();

    request_json(
        &app,
        "PUT",
        format!("/runs/{child_id}/parent"),
        serde_json::json!({ "parent_id": parent_id }),
        StatusCode::OK,
    )
    .await;

    request_json(
        &app,
        "PUT",
        format!("/runs/{parent_id}/parent"),
        serde_json::json!({ "parent_id": child_id }),
        StatusCode::BAD_REQUEST,
    )
    .await;
    request_json(
        &app,
        "PUT",
        format!("/runs/{child_id}/parent"),
        serde_json::json!({ "parent_id": child_id }),
        StatusCode::BAD_REQUEST,
    )
    .await;
    request_json(
        &app,
        "PUT",
        format!("/runs/{child_id}/parent"),
        serde_json::json!({ "parent_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV" }),
        StatusCode::NOT_FOUND,
    )
    .await;

    let missing_child = Request::builder()
        .method("DELETE")
        .uri(api("/runs/01ARZ3NDEKTSV4RRFFQ69G5FAV/parent"))
        .body(Body::empty())
        .unwrap();
    response_status(
        app.oneshot(missing_child).await.unwrap(),
        StatusCode::NOT_FOUND,
        "DELETE /api/v1/runs/{missing}/parent",
    )
    .await;
}
