mod common;

use serde_json::json;

#[tokio::test]
async fn error_response_model_not_found() {
    let neuron_url = common::spawn_mock_neuron().await;
    let gateway_url = common::spawn_gateway(&neuron_url).await;

    let client = reqwest::Client::new();

    // Request a model that isn't loaded on the mock neuron.
    let resp = client
        .post(format!("{gateway_url}/v1/chat/completions"))
        .header("Content-Type", "application/json")
        .json(&json!({
            "model": "nonexistent-model",
            "messages": [{"role": "user", "content": "hi"}]
        }))
        .send()
        .await
        .expect("request should succeed");

    assert_eq!(resp.status(), axum::http::StatusCode::NOT_FOUND);

    let body: serde_json::Value = resp.json().await.expect("valid json");
    let err = body.get("error").expect("response has error object");

    // Broad type categorization
    assert_eq!(err.get("type").unwrap(), "invalid_request_error");
    // Specific machine-readable code
    assert_eq!(
        err.get("code").unwrap().as_str().unwrap(),
        "model_not_found"
    );
    // param is always null
    assert!(err.get("param").unwrap().is_null());
}

#[tokio::test]
async fn error_response_missing_model_field() {
    let neuron_url = common::spawn_mock_neuron().await;
    let gateway_url = common::spawn_gateway(&neuron_url).await;

    let client = reqwest::Client::new();

    // Request without the required `model` field.
    let resp = client
        .post(format!("{gateway_url}/v1/chat/completions"))
        .header("Content-Type", "application/json")
        .json(&json!({
            "messages": [{"role": "user", "content": "hi"}]
        }))
        .send()
        .await
        .expect("request should succeed");

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

    let body: serde_json::Value = resp.json().await.expect("valid json");
    let err = body.get("error").expect("response has error object");

    assert_eq!(err.get("type").unwrap(), "invalid_request_error");
    assert_eq!(
        err.get("code").unwrap().as_str().unwrap(),
        "missing_model_field"
    );
    assert!(err.get("param").unwrap().is_null());
}

#[tokio::test]
async fn error_response_no_healthy_nodes() {
    use cortex_core::config::{EvictionSettings, GatewayConfig, GatewaySettings, NeuronEndpoint};
    use std::sync::Arc;

    // Create a gateway config with a neuron pointing at an unreachable port so no node is ever healthy.
    let config = GatewayConfig {
        gateway: GatewaySettings {
            listen: "127.0.0.1:0".into(),
            metrics_listen: "127.0.0.1:0".into(),
        },
        eviction: EvictionSettings {
            strategy: cortex_core::config::EvictionStrategy::Lru,
            defrag_after_cycles: 0,
        },
        neurons: vec![NeuronEndpoint {
            name: "dead-node".into(),
            endpoint: "http://127.0.0.1:1".into(),
        }],
        models_config: "/dev/null".into(),
        entitlements: Default::default(),
        upstream: Default::default(),
    };

    let fleet = Arc::new(cortex_gateway::state::CortexState::from_config(&config));

    let app = cortex_gateway::build_app(fleet);
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    tokio::spawn(async move {
        axum::serve(listener, app).await.unwrap();
    });

    // Allow the poller a moment to mark the node unhealthy.
    tokio::time::sleep(std::time::Duration::from_millis(200)).await;

    let client = reqwest::Client::new();
    let resp = client
        .post(format!("http://{addr}/v1/chat/completions"))
        .header("Content-Type", "application/json")
        .json(&json!({
            "model": "any-model",
            "messages": [{"role": "user", "content": "hi"}]
        }))
        .send()
        .await
        .expect("request should succeed");

    assert_eq!(resp.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);

    // Transient 503 — the gateway advertises Retry-After so OpenAI-compatible
    // clients back off and retry rather than surfacing an opaque error (#63).
    let retry_after = resp
        .headers()
        .get(reqwest::header::RETRY_AFTER)
        .expect("transient 503 must carry Retry-After")
        .to_str()
        .unwrap()
        .to_string();
    assert_eq!(retry_after, "5");

    let body: serde_json::Value = resp.json().await.expect("valid json");
    let err = body.get("error").expect("response has error object");

    assert_eq!(err.get("type").unwrap(), "api_error");
    assert_eq!(
        err.get("code").unwrap().as_str().unwrap(),
        "service_unavailable"
    );
    assert!(err.get("param").unwrap().is_null());
}
