use std::path::{Path, PathBuf};

use axum::body::Body;
use axum::http::{Method, Request, StatusCode, header};
use fabro_db::Database;
use fabro_mcp_store::McpServerStore;
use fabro_server::server::build_router;
use fabro_server::test_support::{TestAppStateBuilder, build_test_router, test_auth_mode};
use fabro_types::settings::McpTransport;
use fabro_types::{McpServerDefinition, McpServerId};
use serde_json::{Value, json};
use tower::ServiceExt;

use crate::helpers::{
    MINIMAL_DOT, checked_response, minimal_manifest_json, response_json, response_status,
};

fn mcp_server_body(id: &str, display_name: &str) -> Value {
    json!({
        "id": id,
        "display_name": display_name,
        "description": "Production MCP server.",
        "transport": {
            "type": "http",
            "protocol": "streamable_http",
            "url": "https://example.com/mcp",
            "headers": {
                "X-Org": "fabro"
            }
        },
        "startup_timeout_secs": 10,
        "tool_timeout_secs": 60
    })
}

fn replacement_body(display_name: &str) -> Value {
    json!({
        "display_name": display_name,
        "description": null,
        "transport": {
            "type": "http",
            "protocol": "sse",
            "url": "https://example.com/mcp/v2",
            "headers": {}
        },
        "startup_timeout_secs": 15,
        "tool_timeout_secs": 90
    })
}

fn mcp_server_app() -> (axum::Router, tempfile::TempDir, PathBuf) {
    let temp_dir = tempfile::tempdir().expect("mcp server 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 db_path = temp_dir.path().join("db").join("fabro.sqlite3");
    let state = TestAppStateBuilder::new()
        .active_config_path(active_config_path)
        .vault_path(vault_path)
        .build();
    (build_test_router(state), temp_dir, db_path)
}

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

fn empty_request(method: Method, path: &str) -> Request<Body> {
    Request::builder()
        .method(method)
        .uri(crate::helpers::api(path))
        .body(Body::empty())
        .expect("mcp server 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(crate::helpers::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("mcp server fixture should serialize"))
        }
        None => Body::empty(),
    };
    builder
        .body(body)
        .expect("mcp server If-Match request should build")
}

async fn create_mcp_server(app: &axum::Router, id: &str, name: &str) -> Value {
    let response = app
        .clone()
        .oneshot(json_request(
            Method::POST,
            "/mcp-servers",
            &mcp_server_body(id, name),
        ))
        .await
        .expect("create mcp server should respond");
    response_json(response, StatusCode::CREATED, "POST /api/v1/mcp-servers").await
}

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

async fn persisted_mcp_server(db_path: &Path, id: &str) -> Option<McpServerDefinition> {
    let database = Database::connect(db_path)
        .await
        .expect("persisted MCP server database should connect");
    let store = McpServerStore::load(database.clone_pool())
        .await
        .expect("persisted MCP server store should load");
    store.get(&McpServerId::new(id).expect("fixture MCP server id should be valid"))
}

#[tokio::test]
async fn empty_mcp_server_list_returns_total_zero() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();

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

    assert_eq!(
        body,
        json!({
            "data": [],
            "meta": { "total": 0 }
        })
    );
}

#[tokio::test]
async fn create_mcp_server_returns_etag_and_persists_sqlite_row() {
    let (app, _temp_dir, db_path) = mcp_server_app();

    let response = app
        .clone()
        .oneshot(json_request(
            Method::POST,
            "/mcp-servers",
            &mcp_server_body("sentry", "Sentry"),
        ))
        .await
        .expect("create mcp server should respond");
    let response =
        checked_response(response, StatusCode::CREATED, "POST /api/v1/mcp-servers").await;
    let etag = response
        .headers()
        .get(header::ETAG)
        .expect("create mcp server 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["id"], "sentry");
    assert_eq!(body["display_name"], "Sentry");
    assert_eq!(etag, format!("\"{}\"", revision_from(&body)));
    assert!(db_path.exists());

    // The response is the value-omitting view: header *names* are returned, but
    // the stored header value is not.
    assert_eq!(body["transport"]["header_keys"], json!(["X-Org"]));
    assert!(
        body["transport"].get("headers").is_none(),
        "response must not echo transport header values"
    );

    let persisted = persisted_mcp_server(&db_path, "sentry")
        .await
        .expect("MCP server should be persisted");
    assert_eq!(persisted.display_name, "Sentry");
    assert_eq!(persisted.revision.as_str(), revision_from(&body));
    let McpTransport::Http { headers, .. } = persisted.transport else {
        panic!("persisted MCP server should use HTTP transport")
    };
    assert_eq!(headers.get("X-Org").map(String::as_str), Some("fabro"));
}

#[tokio::test]
async fn mcp_server_round_trips_through_create_get_and_sqlite() {
    let (app, _temp_dir, db_path) = mcp_server_app();

    let created = create_mcp_server(&app, "sentry", "Sentry").await;
    assert_eq!(created["transport"]["type"], "http");
    assert_eq!(created["transport"]["url"], "https://example.com/mcp");

    let response = app
        .clone()
        .oneshot(empty_request(Method::GET, "/mcp-servers/sentry"))
        .await
        .expect("get mcp server should respond");
    let retrieved = response_json(response, StatusCode::OK, "GET /api/v1/mcp-servers/sentry").await;
    assert_eq!(retrieved, created);

    let persisted = persisted_mcp_server(&db_path, "sentry")
        .await
        .expect("MCP server should be persisted");
    assert!(matches!(persisted.transport, McpTransport::Http { .. }));
}

#[tokio::test]
async fn list_mcp_servers_returns_items_sorted_by_id() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    create_mcp_server(&app, "zulu", "Zulu").await;
    create_mcp_server(&app, "alpha", "Alpha").await;

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

    assert_eq!(body["meta"]["total"], 2);
    assert_eq!(body["data"][0]["id"], "alpha");
    assert_eq!(body["data"][1]["id"], "zulu");
}

#[tokio::test]
async fn duplicate_mcp_server_create_returns_conflict() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    create_mcp_server(&app, "sentry", "Sentry").await;

    let response = app
        .oneshot(json_request(
            Method::POST,
            "/mcp-servers",
            &mcp_server_body("sentry", "Duplicate"),
        ))
        .await
        .expect("duplicate create should respond");

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

#[tokio::test]
async fn get_mcp_server_returns_current_etag() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    let created = create_mcp_server(&app, "sentry", "Sentry").await;
    let revision = revision_from(&created);

    let response = app
        .oneshot(empty_request(Method::GET, "/mcp-servers/sentry"))
        .await
        .expect("get mcp server should respond");
    let response =
        checked_response(response, StatusCode::OK, "GET /api/v1/mcp-servers/sentry").await;

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

#[tokio::test]
async fn get_missing_mcp_server_returns_not_found() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();

    let response = app
        .oneshot(empty_request(Method::GET, "/mcp-servers/missing"))
        .await
        .expect("get missing mcp server should respond");

    response_status(
        response,
        StatusCode::NOT_FOUND,
        "GET /api/v1/mcp-servers/missing",
    )
    .await;
}

#[tokio::test]
async fn replace_mcp_server_accepts_unquoted_if_match_and_returns_new_etag() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    let created = create_mcp_server(&app, "sentry", "Sentry").await;
    let revision = revision_from(&created);

    let response = app
        .oneshot(request_with_if_match(
            Method::PUT,
            "/mcp-servers/sentry",
            revision,
            Some(replacement_body("Sentry v2")),
        ))
        .await
        .expect("replace mcp server should respond");
    let response =
        checked_response(response, StatusCode::OK, "PUT /api/v1/mcp-servers/sentry").await;
    let etag = response
        .headers()
        .get(header::ETAG)
        .expect("PUT mcp server 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["display_name"], "Sentry v2");
    assert_eq!(body["transport"]["protocol"], "sse");
    assert_ne!(body["revision"], revision);
    assert_eq!(etag, format!("\"{}\"", revision_from(&body)));
}

#[tokio::test]
async fn replace_mcp_server_accepts_quoted_if_match() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    let created = create_mcp_server(&app, "sentry", "Sentry").await;
    let revision = revision_from(&created);

    let response = app
        .oneshot(request_with_if_match(
            Method::PUT,
            "/mcp-servers/sentry",
            &format!("\"{revision}\""),
            Some(replacement_body("Sentry v2")),
        ))
        .await
        .expect("replace mcp server with quoted If-Match should respond");

    response_status(
        response,
        StatusCode::OK,
        "PUT /api/v1/mcp-servers/sentry quoted If-Match",
    )
    .await;
}

#[tokio::test]
async fn stale_mcp_server_replace_returns_conflict() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    let created = create_mcp_server(&app, "sentry", "Sentry").await;
    let stale_revision = revision_from(&created).to_string();

    let replaced = app
        .clone()
        .oneshot(request_with_if_match(
            Method::PUT,
            "/mcp-servers/sentry",
            &stale_revision,
            Some(replacement_body("Updated")),
        ))
        .await
        .expect("first replace should respond");
    response_status(
        replaced,
        StatusCode::OK,
        "PUT /api/v1/mcp-servers/sentry first replace",
    )
    .await;

    let response = app
        .oneshot(request_with_if_match(
            Method::PUT,
            "/mcp-servers/sentry",
            &stale_revision,
            Some(replacement_body("Stale")),
        ))
        .await
        .expect("stale replace should respond");

    response_status(
        response,
        StatusCode::CONFLICT,
        "PUT /api/v1/mcp-servers/sentry stale",
    )
    .await;
}

#[tokio::test]
async fn replace_and_delete_mcp_server_require_if_match() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    create_mcp_server(&app, "sentry", "Sentry").await;

    let replace_response = app
        .clone()
        .oneshot(json_request(
            Method::PUT,
            "/mcp-servers/sentry",
            &replacement_body("Updated"),
        ))
        .await
        .expect("replace without If-Match should respond");
    response_status(
        replace_response,
        StatusCode::PRECONDITION_REQUIRED,
        "PUT /api/v1/mcp-servers/sentry without If-Match",
    )
    .await;

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

#[tokio::test]
async fn delete_mcp_server_removes_sqlite_row_and_resource() {
    let (app, _temp_dir, db_path) = mcp_server_app();
    let created = create_mcp_server(&app, "sentry", "Sentry").await;
    let revision = revision_from(&created);

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

    assert!(persisted_mcp_server(&db_path, "sentry").await.is_none());
    let response = app
        .oneshot(empty_request(Method::GET, "/mcp-servers/sentry"))
        .await
        .expect("get deleted mcp server should respond");
    response_status(
        response,
        StatusCode::NOT_FOUND,
        "GET /api/v1/mcp-servers/sentry after delete",
    )
    .await;
}

#[tokio::test]
async fn empty_mcp_server_name_is_unprocessable() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    let mut body = mcp_server_body("sentry", "Sentry");
    body["display_name"] = json!(" ");

    let response = app
        .oneshot(json_request(Method::POST, "/mcp-servers", &body))
        .await
        .expect("empty mcp server name create should respond");

    response_status(
        response,
        StatusCode::UNPROCESSABLE_ENTITY,
        "POST /api/v1/mcp-servers empty name",
    )
    .await;
}

#[tokio::test]
async fn empty_transport_command_is_unprocessable() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    let mut body = mcp_server_body("local", "Local");
    body["transport"] = json!({
        "type": "stdio",
        "command": [],
        "env": {}
    });

    let response = app
        .oneshot(json_request(Method::POST, "/mcp-servers", &body))
        .await
        .expect("empty transport command create should respond");

    response_status(
        response,
        StatusCode::UNPROCESSABLE_ENTITY,
        "POST /api/v1/mcp-servers empty transport command",
    )
    .await;
}

#[tokio::test]
async fn unknown_transport_type_is_unprocessable() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    let mut body = mcp_server_body("sentry", "Sentry");
    body["transport"] = json!({
        "type": "carrier-pigeon",
        "url": "https://example.com/mcp"
    });

    let response = app
        .oneshot(json_request(Method::POST, "/mcp-servers", &body))
        .await
        .expect("unknown transport type create should respond");

    response_status(
        response,
        StatusCode::UNPROCESSABLE_ENTITY,
        "POST /api/v1/mcp-servers unknown transport type",
    )
    .await;
}

#[tokio::test]
async fn unknown_transport_field_is_unprocessable() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    let mut body = mcp_server_body("sentry", "Sentry");
    body["transport"]["unexpected"] = json!("typo");

    let response = app
        .oneshot(json_request(Method::POST, "/mcp-servers", &body))
        .await
        .expect("unknown transport field create should respond");

    response_status(
        response,
        StatusCode::UNPROCESSABLE_ENTITY,
        "POST /api/v1/mcp-servers unknown transport field",
    )
    .await;
}

#[tokio::test]
async fn invalid_mcp_server_id_is_bad_request() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();

    let response = app
        .oneshot(empty_request(Method::GET, "/mcp-servers/Bad_Id"))
        .await
        .expect("invalid id get should respond");

    response_status(
        response,
        StatusCode::BAD_REQUEST,
        "GET /api/v1/mcp-servers/Bad_Id",
    )
    .await;
}

#[tokio::test]
async fn created_mcp_server_can_be_referenced_by_manifest_validation() {
    let (app, _temp_dir, _mcp_dir) = mcp_server_app();
    create_mcp_server(&app, "sentry", "Sentry").await;

    let mut manifest = minimal_manifest_json(MINIMAL_DOT);
    manifest["workflows"]["workflow.fabro"]["config"] = json!({
        "path": "workflow.toml",
        "source": r#"
_version = 1

[run.agent.mcps.sentry]
id = "sentry"
"#
    });

    let response = app
        .oneshot(json_request(Method::POST, "/validate", &manifest))
        .await
        .expect("manifest validation should respond");
    let body = response_json(response, StatusCode::OK, "POST /api/v1/validate").await;

    assert_eq!(body["ok"], true);
}

#[tokio::test]
async fn malformed_legacy_mcp_server_toml_fails_startup() {
    let temp_dir = tempfile::tempdir().expect("mcp server test tempdir should be created");
    let mcp_dir = temp_dir.path().join("mcps");
    tokio::fs::create_dir_all(&mcp_dir)
        .await
        .expect("mcp server dir should be created");
    tokio::fs::write(mcp_dir.join("broken.toml"), "not valid toml =")
        .await
        .expect("broken mcp server fixture should be written");

    let result = TestAppStateBuilder::new()
        .active_config_path(temp_dir.path().join("settings.toml"))
        .try_build();

    assert!(result.is_err());
}

#[tokio::test]
async fn legacy_mcp_server_toml_imports_at_startup_and_renames_directory() {
    let temp_dir = tempfile::tempdir().expect("mcp server 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 db_path = temp_dir.path().join("db").join("fabro.sqlite3");
    let mcp_dir = temp_dir.path().join("mcps");
    tokio::fs::create_dir_all(&mcp_dir)
        .await
        .expect("legacy MCP server directory should be created");
    tokio::fs::write(
        mcp_dir.join("sentry.toml"),
        r#"
display_name = "Sentry"
startup_timeout_secs = 10
tool_timeout_secs = 60

[transport]
type = "http"
url = "https://example.com/mcp"

[transport.headers]
Authorization = "secret-value"
"#,
    )
    .await
    .expect("legacy MCP server fixture should be written");

    let state = TestAppStateBuilder::new()
        .active_config_path(active_config_path)
        .vault_path(vault_path)
        .build();
    let app = build_test_router(state);

    let response = app
        .oneshot(empty_request(Method::GET, "/mcp-servers/sentry"))
        .await
        .expect("imported MCP server should respond");
    let body = response_json(
        response,
        StatusCode::OK,
        "GET imported /api/v1/mcp-servers/sentry",
    )
    .await;
    assert_eq!(body["display_name"], "Sentry");
    assert_eq!(body["transport"]["header_keys"], json!(["Authorization"]));
    assert!(!mcp_dir.exists());
    let mut entries = tokio::fs::read_dir(temp_dir.path())
        .await
        .expect("test directory should be readable");
    let mut backup_found = false;
    while let Some(entry) = entries
        .next_entry()
        .await
        .expect("test directory entry should be readable")
    {
        if entry
            .file_name()
            .to_string_lossy()
            .starts_with("mcps.imported-")
        {
            backup_found = true;
            break;
        }
    }
    assert!(backup_found);
    let persisted = persisted_mcp_server(&db_path, "sentry")
        .await
        .expect("imported MCP server should be in SQLite");
    let McpTransport::Http { headers, .. } = persisted.transport else {
        panic!("imported MCP server should use HTTP transport")
    };
    assert_eq!(
        headers.get("Authorization").map(String::as_str),
        Some("secret-value")
    );
}

#[tokio::test]
async fn mcp_servers_routes_require_authenticated_user() {
    let temp_dir = tempfile::tempdir().expect("mcp server 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, "/mcp-servers"))
        .await
        .expect("unauthenticated mcp server list should respond");

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