use std::path::PathBuf;

use axum::body::Body;
use axum::http::{Method, Request, StatusCode, header};
use fabro_config::{RunEnvironmentLayer, RunLayer};
use fabro_server::server::build_router;
use fabro_server::test_support::{
    TestAppStateBuilder, build_test_router, default_test_server_settings, test_auth_mode,
    test_environment_from_storage_dir,
};
use serde_json::{Value, json};
use tower::ServiceExt;

use crate::helpers::{api, checked_response, response_json, response_status};

fn environment_settings(provider: &str) -> Value {
    json!({
        "provider": provider,
        "image": {
            "docker": if provider == "docker" { json!("alpine:3.20") } else { Value::Null },
            "dockerfile": null
        },
        "resources": {
            "cpu": null,
            "memory": null,
            "disk": null
        },
        "network": {
            "mode": "allow_all",
            "allow": []
        },
        "lifecycle": {
            "preserve": false,
            "stop_on_terminal": true,
            "auto_stop": null
        },
        "labels": {},
        "env": {}
    })
}

fn environment_body(id: &str, provider: &str) -> Value {
    let mut body = environment_settings(provider);
    body["id"] = json!(id);
    body
}

fn environment_app() -> (axum::Router, tempfile::TempDir, PathBuf) {
    let temp_dir = tempfile::tempdir().expect("environment test tempdir should be created");
    let active_config_path = temp_dir.path().join("settings.toml");
    let environment_dir = temp_dir.path().join("environments");
    let vault_path = temp_dir.path().join("secrets.json");
    let state = TestAppStateBuilder::new()
        .active_config_path(active_config_path)
        .vault_path(vault_path)
        .build();
    (build_test_router(state), temp_dir, environment_dir)
}

fn environment_app_with_default_environment(
    environment_id: &str,
) -> (axum::Router, tempfile::TempDir) {
    let temp_dir = tempfile::tempdir().expect("environment test tempdir should be created");
    let active_config_path = temp_dir.path().join("settings.toml");
    let vault_path = temp_dir.path().join("secrets.json");
    let manifest_run_defaults = RunLayer {
        environment: Some(RunEnvironmentLayer {
            id: Some(environment_id.to_string()),
            ..RunEnvironmentLayer::default()
        }),
        ..RunLayer::default()
    };
    let state = TestAppStateBuilder::new()
        .runtime_settings(default_test_server_settings(), manifest_run_defaults)
        .active_config_path(active_config_path)
        .vault_path(vault_path)
        .build();
    (build_test_router(state), temp_dir)
}

fn json_request(method: Method, path: &str, body: &Value) -> Request<Body> {
    Request::builder()
        .method(method)
        .uri(api(path))
        .header(header::CONTENT_TYPE, "application/json")
        .body(Body::from(
            serde_json::to_vec(body).expect("environment fixture should serialize"),
        ))
        .expect("environment JSON request should build")
}

fn empty_request(method: Method, path: &str) -> Request<Body> {
    Request::builder()
        .method(method)
        .uri(api(path))
        .body(Body::empty())
        .expect("environment request should build")
}

fn request_with_if_match(
    method: Method,
    path: &str,
    revision: &str,
    body: Option<Value>,
) -> Request<Body> {
    let mut builder = Request::builder()
        .method(method)
        .uri(api(path))
        .header(header::IF_MATCH, revision);
    let body = match body {
        Some(value) => {
            builder = builder.header(header::CONTENT_TYPE, "application/json");
            Body::from(serde_json::to_vec(&value).expect("environment fixture should serialize"))
        }
        None => Body::empty(),
    };
    builder
        .body(body)
        .expect("environment If-Match request should build")
}

async fn create_environment(app: &axum::Router, id: &str, provider: &str) -> Value {
    create_environment_with_body(app, &environment_body(id, provider)).await
}

async fn create_environment_with_body(app: &axum::Router, body: &Value) -> Value {
    let response = app
        .clone()
        .oneshot(json_request(Method::POST, "/environments", body))
        .await
        .expect("create environment should respond");
    response_json(response, StatusCode::CREATED, "POST /api/v1/environments").await
}

fn revision_from(body: &Value) -> &str {
    body["revision"]
        .as_str()
        .expect("environment response should include a revision")
}

async fn persisted_environment(
    temp_dir: &tempfile::TempDir,
    id: &str,
) -> Option<fabro_environment::Environment> {
    test_environment_from_storage_dir(temp_dir.path(), id)
        .await
        .expect("environment store should load from test storage")
}

async fn system_info(app: &axum::Router) -> Value {
    let response = app
        .clone()
        .oneshot(empty_request(Method::GET, "/system/info"))
        .await
        .expect("system info should respond");
    response_json(response, StatusCode::OK, "GET /api/v1/system/info").await
}

#[tokio::test]
async fn list_environments_returns_seeded_catalog_sorted_by_id() {
    let (app, _temp_dir, _environment_dir) = environment_app();

    let response = app
        .oneshot(empty_request(Method::GET, "/environments"))
        .await
        .expect("list environments should respond");
    let body = response_json(response, StatusCode::OK, "GET /api/v1/environments").await;

    assert_eq!(body["meta"]["total"], 2);
    assert_eq!(
        body["data"]
            .as_array()
            .expect("environment list data should be an array")
            .iter()
            .map(|environment| environment["id"]
                .as_str()
                .expect("environment should have id"))
            .collect::<Vec<_>>(),
        vec!["default", "local"]
    );
}

#[tokio::test]
async fn create_environment_persists_to_sqlite_and_is_visible() {
    let (app, temp_dir, environment_dir) = environment_app();
    let mut body = environment_body("custom-env", "docker");
    body["cwd"] = json!("/workspace/custom");

    let created = create_environment_with_body(&app, &body).await;

    assert_eq!(created["id"], "custom-env");
    assert_eq!(created["provider"], "docker");
    assert_eq!(created["cwd"], "/workspace/custom");
    assert!(!environment_dir.join("custom-env.toml").exists());

    let retrieved = app
        .clone()
        .oneshot(empty_request(Method::GET, "/environments/custom-env"))
        .await
        .expect("get environment should respond");
    let retrieved = response_json(
        retrieved,
        StatusCode::OK,
        "GET /api/v1/environments/custom-env",
    )
    .await;
    assert_eq!(retrieved["id"], "custom-env");
    assert_eq!(retrieved["cwd"], "/workspace/custom");

    let list = app
        .oneshot(empty_request(Method::GET, "/environments"))
        .await
        .expect("list environments should respond");
    let list = response_json(list, StatusCode::OK, "GET /api/v1/environments").await;
    assert_eq!(list["meta"]["total"], 3);
    assert!(
        list["data"]
            .as_array()
            .expect("environment list data should be an array")
            .iter()
            .any(|environment| environment["id"] == "custom-env")
    );

    let persisted = persisted_environment(&temp_dir, "custom-env")
        .await
        .expect("custom environment should persist to SQLite");
    assert_eq!(persisted.settings.provider.to_string(), "docker");
    assert_eq!(persisted.settings.cwd.as_deref(), Some("/workspace/custom"));
}

#[tokio::test]
async fn get_environment_returns_current_etag() {
    let (app, _temp_dir, _environment_dir) = environment_app();
    let created = create_environment(&app, "etag-env", "local").await;
    let revision = revision_from(&created);

    let response = app
        .oneshot(empty_request(Method::GET, "/environments/etag-env"))
        .await
        .expect("get environment should respond");
    let response = checked_response(
        response,
        StatusCode::OK,
        "GET /api/v1/environments/etag-env",
    )
    .await;

    assert_eq!(
        response
            .headers()
            .get(header::ETAG)
            .expect("GET environment should include ETag"),
        &format!("\"{revision}\"")
    );
    let body = crate::helpers::body_json(response.into_body()).await;
    assert_eq!(body["revision"], revision);
}

#[tokio::test]
async fn replace_environment_updates_sqlite_and_returns_new_etag() {
    let (app, temp_dir, environment_dir) = environment_app();
    let created = create_environment(&app, "replace-env", "docker").await;
    let revision = revision_from(&created);
    let mut replacement = environment_settings("local");
    replacement["labels"] = json!({ "tier": "dev" });
    replacement["cwd"] = json!("/srv/fabro/local");

    let response = app
        .oneshot(request_with_if_match(
            Method::PUT,
            "/environments/replace-env",
            revision,
            Some(replacement),
        ))
        .await
        .expect("replace environment should respond");
    let response = checked_response(
        response,
        StatusCode::OK,
        "PUT /api/v1/environments/replace-env",
    )
    .await;
    let etag = response
        .headers()
        .get(header::ETAG)
        .expect("PUT environment should include ETag")
        .to_str()
        .expect("ETag should be ASCII")
        .to_string();
    let body = crate::helpers::body_json(response.into_body()).await;

    assert_eq!(body["provider"], "local");
    assert_eq!(body["labels"]["tier"], "dev");
    assert_eq!(body["cwd"], "/srv/fabro/local");
    assert_ne!(body["revision"], revision);
    assert_eq!(etag, format!("\"{}\"", revision_from(&body)));
    assert!(!environment_dir.join("replace-env.toml").exists());
    let persisted = persisted_environment(&temp_dir, "replace-env")
        .await
        .expect("replacement should persist to SQLite");
    assert_eq!(
        persisted.settings.labels.get("tier").map(String::as_str),
        Some("dev")
    );
    assert_eq!(persisted.settings.cwd.as_deref(), Some("/srv/fabro/local"));
}

#[tokio::test]
async fn replace_and_delete_environment_require_if_match() {
    let (app, _temp_dir, _environment_dir) = environment_app();
    create_environment(&app, "match-env", "local").await;

    let replace_response = app
        .clone()
        .oneshot(json_request(
            Method::PUT,
            "/environments/match-env",
            &environment_settings("docker"),
        ))
        .await
        .expect("replace without If-Match should respond");
    response_status(
        replace_response,
        StatusCode::PRECONDITION_REQUIRED,
        "PUT /api/v1/environments/match-env without If-Match",
    )
    .await;

    let delete_response = app
        .oneshot(empty_request(Method::DELETE, "/environments/match-env"))
        .await
        .expect("delete without If-Match should respond");
    response_status(
        delete_response,
        StatusCode::PRECONDITION_REQUIRED,
        "DELETE /api/v1/environments/match-env without If-Match",
    )
    .await;
}

#[tokio::test]
async fn stale_environment_replace_and_delete_return_conflict() {
    let (app, _temp_dir, _environment_dir) = environment_app();
    let created = create_environment(&app, "stale-env", "docker").await;
    let stale_revision = revision_from(&created).to_string();

    let replaced = app
        .clone()
        .oneshot(request_with_if_match(
            Method::PUT,
            "/environments/stale-env",
            &stale_revision,
            Some(environment_settings("local")),
        ))
        .await
        .expect("first replace should respond");
    response_status(
        replaced,
        StatusCode::OK,
        "PUT /api/v1/environments/stale-env first replace",
    )
    .await;

    let stale_replace = app
        .clone()
        .oneshot(request_with_if_match(
            Method::PUT,
            "/environments/stale-env",
            &stale_revision,
            Some(environment_settings("docker")),
        ))
        .await
        .expect("stale replace should respond");
    response_status(
        stale_replace,
        StatusCode::CONFLICT,
        "PUT /api/v1/environments/stale-env stale",
    )
    .await;

    let stale_delete = app
        .oneshot(request_with_if_match(
            Method::DELETE,
            "/environments/stale-env",
            &stale_revision,
            None,
        ))
        .await
        .expect("stale delete should respond");
    response_status(
        stale_delete,
        StatusCode::CONFLICT,
        "DELETE /api/v1/environments/stale-env stale",
    )
    .await;
}

#[tokio::test]
async fn duplicate_environment_create_returns_conflict() {
    let (app, _temp_dir, _environment_dir) = environment_app();
    create_environment(&app, "duplicate-env", "local").await;

    let response = app
        .oneshot(json_request(
            Method::POST,
            "/environments",
            &environment_body("duplicate-env", "docker"),
        ))
        .await
        .expect("duplicate create should respond");

    response_status(
        response,
        StatusCode::CONFLICT,
        "POST /api/v1/environments duplicate",
    )
    .await;
}

#[tokio::test]
async fn reserved_local_environment_cannot_be_created_or_modified() {
    let (app, _temp_dir, _environment_dir) = environment_app();

    // `local` is reserved: creating it is rejected with a conflict.
    let created = app
        .clone()
        .oneshot(json_request(
            Method::POST,
            "/environments",
            &environment_body("local", "local"),
        ))
        .await
        .expect("reserved create should respond");
    response_status(
        created,
        StatusCode::CONFLICT,
        "POST /api/v1/environments local",
    )
    .await;

    // It is synthesized in memory (local provider enabled) and readable.
    let local = app
        .clone()
        .oneshot(empty_request(Method::GET, "/environments/local"))
        .await
        .expect("get local should respond");
    let local = response_json(local, StatusCode::OK, "GET /api/v1/environments/local").await;
    let revision = revision_from(&local);

    // Replace and delete are rejected even with a valid If-Match.
    let replaced = app
        .clone()
        .oneshot(request_with_if_match(
            Method::PUT,
            "/environments/local",
            &format!("\"{revision}\""),
            Some(environment_settings("local")),
        ))
        .await
        .expect("reserved replace should respond");
    response_status(
        replaced,
        StatusCode::CONFLICT,
        "PUT /api/v1/environments/local",
    )
    .await;

    let deleted = app
        .oneshot(request_with_if_match(
            Method::DELETE,
            "/environments/local",
            &format!("\"{revision}\""),
            None,
        ))
        .await
        .expect("reserved delete should respond");
    response_status(
        deleted,
        StatusCode::CONFLICT,
        "DELETE /api/v1/environments/local",
    )
    .await;
}

#[tokio::test]
async fn invalid_environment_id_and_if_match_return_bad_request() {
    let (app, _temp_dir, _environment_dir) = environment_app();

    let invalid_id = app
        .clone()
        .oneshot(empty_request(Method::GET, "/environments/Bad!"))
        .await
        .expect("invalid id request should respond");
    response_status(
        invalid_id,
        StatusCode::BAD_REQUEST,
        "GET /api/v1/environments/Bad!",
    )
    .await;

    create_environment(&app, "header-env", "local").await;
    let invalid_header = app
        .oneshot(request_with_if_match(
            Method::PUT,
            "/environments/header-env",
            "not-a-revision",
            Some(environment_settings("docker")),
        ))
        .await
        .expect("invalid If-Match request should respond");
    response_status(
        invalid_header,
        StatusCode::BAD_REQUEST,
        "PUT /api/v1/environments/header-env invalid If-Match",
    )
    .await;
}

#[tokio::test]
async fn invalid_environment_settings_return_unprocessable_entity() {
    let (app, _temp_dir, _environment_dir) = environment_app();
    let mut body = environment_body("invalid-env", "local");
    body["network"]["mode"] = json!("block");

    let response = app
        .oneshot(json_request(Method::POST, "/environments", &body))
        .await
        .expect("invalid environment create should respond");

    response_status(
        response,
        StatusCode::UNPROCESSABLE_ENTITY,
        "POST /api/v1/environments invalid settings",
    )
    .await;
}

#[tokio::test]
async fn relative_environment_cwd_over_rest_returns_unprocessable_entity() {
    let (app, temp_dir, environment_dir) = environment_app();
    let mut body = environment_body("relative-cwd", "local");
    body["cwd"] = json!("relative/workspace");

    let response = app
        .oneshot(json_request(Method::POST, "/environments", &body))
        .await
        .expect("invalid cwd create should respond");
    let error = response_json(
        response,
        StatusCode::UNPROCESSABLE_ENTITY,
        "POST /api/v1/environments relative cwd",
    )
    .await;

    assert!(!environment_dir.join("relative-cwd.toml").exists());
    assert!(
        persisted_environment(&temp_dir, "relative-cwd")
            .await
            .is_none()
    );
    let message = serde_json::to_string(&error).expect("error should serialize");
    assert!(
        message.contains("environment.cwd") && message.contains("absolute path"),
        "unexpected error: {message}"
    );
}

#[tokio::test]
async fn dockerfile_path_over_rest_is_rejected_without_persisting_or_exposing_contents() {
    let (app, temp_dir, environment_dir) = environment_app();
    tokio::fs::write(
        temp_dir.path().join("Dockerfile"),
        "FROM private.example/secret\n",
    )
    .await
    .expect("secret Dockerfile fixture should be written");
    let mut body = environment_body("path-env", "docker");
    body["image"]["docker"] = Value::Null;
    body["image"]["dockerfile"] = json!({
        "type": "path",
        "path": "Dockerfile"
    });

    let response = app
        .clone()
        .oneshot(json_request(Method::POST, "/environments", &body))
        .await
        .expect("path Dockerfile create should respond");
    let error = response_json(
        response,
        StatusCode::UNPROCESSABLE_ENTITY,
        "POST /api/v1/environments Dockerfile path",
    )
    .await;

    assert!(!environment_dir.join("path-env.toml").exists());
    assert!(persisted_environment(&temp_dir, "path-env").await.is_none());
    assert!(
        !serde_json::to_string(&error)
            .expect("error body should serialize")
            .contains("private.example/secret")
    );
    let list = app
        .oneshot(empty_request(Method::GET, "/environments"))
        .await
        .expect("list environments should respond");
    let list = response_json(list, StatusCode::OK, "GET /api/v1/environments").await;
    assert!(
        !list["data"]
            .as_array()
            .expect("environment list data should be an array")
            .iter()
            .any(|environment| environment["id"] == "path-env")
    );
}

#[tokio::test]
async fn delete_environment_removes_non_default_and_default_is_deletable() {
    let (app, temp_dir, environment_dir) = environment_app();
    let created = create_environment(&app, "delete-env", "local").await;
    let revision = revision_from(&created);

    let response = app
        .clone()
        .oneshot(request_with_if_match(
            Method::DELETE,
            "/environments/delete-env",
            &format!("\"{revision}\""),
            None,
        ))
        .await
        .expect("delete environment should respond");
    response_status(
        response,
        StatusCode::NO_CONTENT,
        "DELETE /api/v1/environments/delete-env",
    )
    .await;

    assert!(!environment_dir.join("delete-env.toml").exists());
    assert!(
        persisted_environment(&temp_dir, "delete-env")
            .await
            .is_none()
    );
    let missing = app
        .clone()
        .oneshot(empty_request(Method::GET, "/environments/delete-env"))
        .await
        .expect("get deleted environment should respond");
    response_status(
        missing,
        StatusCode::NOT_FOUND,
        "GET /api/v1/environments/delete-env after delete",
    )
    .await;

    // `default` is an ordinary environment: it can be deleted, which removes the
    // run fallback. The server no longer protects it.
    let default = app
        .clone()
        .oneshot(empty_request(Method::GET, "/environments/default"))
        .await
        .expect("get default environment should respond");
    let default = response_json(default, StatusCode::OK, "GET /api/v1/environments/default").await;
    let deleted = app
        .clone()
        .oneshot(request_with_if_match(
            Method::DELETE,
            "/environments/default",
            revision_from(&default),
            None,
        ))
        .await
        .expect("delete default environment should respond");
    response_status(
        deleted,
        StatusCode::NO_CONTENT,
        "DELETE /api/v1/environments/default",
    )
    .await;

    assert!(!environment_dir.join("default.toml").exists());
    assert!(persisted_environment(&temp_dir, "default").await.is_none());
    let missing_default = app
        .oneshot(empty_request(Method::GET, "/environments/default"))
        .await
        .expect("get deleted default environment should respond");
    response_status(
        missing_default,
        StatusCode::NOT_FOUND,
        "GET /api/v1/environments/default after delete",
    )
    .await;
}

#[tokio::test]
async fn environment_routes_require_authenticated_user() {
    let temp_dir = tempfile::tempdir().expect("environment test tempdir should be created");
    let state = TestAppStateBuilder::new()
        .active_config_path(temp_dir.path().join("settings.toml"))
        .build();
    let app = build_router(state, test_auth_mode());

    let response = app
        .oneshot(empty_request(Method::GET, "/environments"))
        .await
        .expect("unauthenticated environment list should respond");

    response_status(
        response,
        StatusCode::UNAUTHORIZED,
        "GET /api/v1/environments without auth",
    )
    .await;
}

#[tokio::test]
async fn create_environment_refreshes_cached_manifest_run_settings() {
    let (app, _temp_dir) = environment_app_with_default_environment("api-default");

    let before = system_info(&app).await;
    assert_eq!(before["sandbox_provider"], "local");

    create_environment(&app, "api-default", "daytona").await;

    let after = system_info(&app).await;
    assert_eq!(after["sandbox_provider"], "daytona");
}
