use std::sync::Arc;

use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::post, Json, Router};
use cc_switch_lib::{Database, Provider, ProviderMeta, ProxyService};
use serde_json::{json, Map, Value};
use serial_test::serial;
use tokio::sync::Mutex;

async fn bind_test_listener() -> tokio::net::TcpListener {
    let mut last_error = None;
    for _ in 0..20 {
        match tokio::net::TcpListener::bind("127.0.0.1:0").await {
            Ok(listener) => return listener,
            Err(error) => {
                last_error = Some(error);
                tokio::time::sleep(std::time::Duration::from_millis(25)).await;
            }
        }
    }

    panic!(
        "bind upstream listener: {:?}",
        last_error.expect("listener bind should produce an error")
    );
}

#[derive(Clone, Default)]
struct UpstreamState {
    request_body: Arc<Mutex<Option<Value>>>,
}

#[derive(Clone, Copy)]
enum UpstreamFormat {
    OpenAiChat,
    Anthropic,
}

struct MappingCase {
    provider_env: Map<String, Value>,
    request_body: Value,
    expected_model: &'static str,
    upstream_format: UpstreamFormat,
}

async fn handle_openai_chat(
    State(state): State<UpstreamState>,
    Json(body): Json<Value>,
) -> impl IntoResponse {
    *state.request_body.lock().await = Some(body);

    (
        StatusCode::OK,
        Json(json!({
            "id": "chatcmpl-model-mapping",
            "object": "chat.completion",
            "created": 123,
            "model": "gpt-4o-mini",
            "choices": [{
                "index": 0,
                "message": {
                    "role": "assistant",
                    "content": "ok"
                },
                "finish_reason": "stop"
            }],
            "usage": {
                "prompt_tokens": 1,
                "completion_tokens": 1,
                "total_tokens": 2
            }
        })),
    )
}

async fn handle_anthropic_messages(
    State(state): State<UpstreamState>,
    Json(body): Json<Value>,
) -> impl IntoResponse {
    *state.request_body.lock().await = Some(body.clone());

    (
        StatusCode::OK,
        Json(json!({
            "id": "msg_model_mapping",
            "type": "message",
            "role": "assistant",
            "content": [{
                "type": "text",
                "text": "ok"
            }],
            "model": body.get("model").cloned().unwrap_or_else(|| json!("")),
            "stop_reason": "end_turn",
            "stop_sequence": null,
            "usage": {
                "input_tokens": 1,
                "output_tokens": 1
            }
        })),
    )
}

async fn assert_forwarded_model(case: MappingCase) {
    let upstream_state = UpstreamState::default();
    let upstream_router = match case.upstream_format {
        UpstreamFormat::OpenAiChat => Router::new()
            .route("/v1/chat/completions", post(handle_openai_chat))
            .with_state(upstream_state.clone()),
        UpstreamFormat::Anthropic => Router::new()
            .route("/v1/messages", post(handle_anthropic_messages))
            .with_state(upstream_state.clone()),
    };

    let upstream_listener = bind_test_listener().await;
    let upstream_addr = upstream_listener
        .local_addr()
        .expect("read upstream address");
    let upstream_handle = tokio::spawn(async move {
        let _ = axum::serve(upstream_listener, upstream_router).await;
    });

    let db = Arc::new(Database::memory().expect("create memory database"));
    let mut env = case.provider_env;
    env.insert(
        "ANTHROPIC_BASE_URL".to_string(),
        json!(format!("http://{}", upstream_addr)),
    );
    env.insert("ANTHROPIC_API_KEY".to_string(), json!("sk-test-claude"));

    let provider = Provider {
        id: "claude-model-mapping".to_string(),
        name: "Claude Model Mapping".to_string(),
        settings_config: json!({ "env": env }),
        website_url: None,
        category: Some("claude".to_string()),
        created_at: None,
        sort_index: None,
        notes: None,
        meta: match case.upstream_format {
            UpstreamFormat::OpenAiChat => Some(ProviderMeta {
                api_format: Some("openai_chat".to_string()),
                ..ProviderMeta::default()
            }),
            UpstreamFormat::Anthropic => None,
        },
        icon: None,
        icon_color: None,
        in_failover_queue: false,
    };
    db.save_provider("claude", &provider)
        .expect("save test provider");
    db.set_current_provider("claude", &provider.id)
        .expect("set current provider");

    let service = ProxyService::new(db);
    let mut config = service.get_config().await.expect("read proxy config");
    config.listen_port = 0;
    service
        .update_config(&config)
        .await
        .expect("update proxy config");

    let proxy = service.start().await.expect("start proxy service");
    let client = reqwest::Client::new();
    let response = client
        .post(format!(
            "http://{}:{}/v1/messages",
            proxy.address, proxy.port
        ))
        .header("anthropic-version", "2023-06-01")
        .json(&case.request_body)
        .send()
        .await
        .expect("send request to proxy");

    assert!(
        response.status().is_success(),
        "proxy should forward the request successfully"
    );

    let upstream_body = upstream_state
        .request_body
        .lock()
        .await
        .clone()
        .expect("upstream should receive request body");
    assert_eq!(
        upstream_body.get("model").and_then(|value| value.as_str()),
        Some(case.expected_model)
    );

    service.stop().await.expect("stop proxy service");
    upstream_handle.abort();
}

fn anthropic_request(model: &str) -> Value {
    json!({
        "model": model,
        "max_tokens": 64,
        "messages": [{
            "role": "user",
            "content": "hello"
        }]
    })
}

#[tokio::test]
#[serial]
async fn haiku_request_uses_haiku_model_override() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_DEFAULT_HAIKU_MODEL".to_string(),
        json!("haiku-mapped"),
    );

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body: anthropic_request("claude-3-5-haiku-20241022"),
        expected_model: "haiku-mapped",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn sonnet_request_uses_sonnet_model_override() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(),
        json!("sonnet-mapped"),
    );

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body: anthropic_request("claude-3-7-sonnet-20250219"),
        expected_model: "sonnet-mapped",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn opus_request_uses_opus_model_override() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_DEFAULT_OPUS_MODEL".to_string(),
        json!("opus-mapped"),
    );

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body: anthropic_request("claude-3-opus-20240229"),
        expected_model: "opus-mapped",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn thinking_enabled_request_uses_reasoning_model_override() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_REASONING_MODEL".to_string(),
        json!("reasoning-mapped"),
    );
    let mut request_body = anthropic_request("claude-3-7-sonnet-20250219");
    request_body["thinking"] = json!({ "type": "enabled" });

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body,
        expected_model: "reasoning-mapped",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn thinking_adaptive_request_uses_reasoning_model_override() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_REASONING_MODEL".to_string(),
        json!("reasoning-mapped"),
    );
    let mut request_body = anthropic_request("claude-3-7-sonnet-20250219");
    request_body["thinking"] = json!({ "type": "adaptive" });

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body,
        expected_model: "reasoning-mapped",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn reasoning_only_config_keeps_non_thinking_request_model() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_REASONING_MODEL".to_string(),
        json!("reasoning-mapped"),
    );

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body: anthropic_request("claude-3-7-sonnet-20250219"),
        expected_model: "claude-3-7-sonnet-20250219",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn unknown_thinking_type_treats_request_as_disabled() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_REASONING_MODEL".to_string(),
        json!("reasoning-mapped"),
    );
    provider_env.insert(
        "ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(),
        json!("sonnet-mapped"),
    );
    let mut request_body = anthropic_request("claude-3-7-sonnet-20250219");
    request_body["thinking"] = json!({ "type": "experimental" });

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body,
        expected_model: "sonnet-mapped",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn model_matching_is_case_insensitive() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(),
        json!("sonnet-mapped"),
    );

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body: anthropic_request("Claude-3-7-SONNET-20250219"),
        expected_model: "sonnet-mapped",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn default_model_fallback_uses_anthropic_model_override() {
    let mut provider_env = Map::new();
    provider_env.insert("ANTHROPIC_MODEL".to_string(), json!("default-mapped"));

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body: anthropic_request("claude-unknown-model"),
        expected_model: "default-mapped",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn no_mapping_config_keeps_original_model() {
    assert_forwarded_model(MappingCase {
        provider_env: Map::new(),
        request_body: anthropic_request("claude-3-7-sonnet-20250219"),
        expected_model: "claude-3-7-sonnet-20250219",
        upstream_format: UpstreamFormat::OpenAiChat,
    })
    .await;
}

#[tokio::test]
#[serial]
async fn anthropic_direct_path_uses_mapped_model() {
    let mut provider_env = Map::new();
    provider_env.insert(
        "ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(),
        json!("sonnet-mapped"),
    );

    assert_forwarded_model(MappingCase {
        provider_env,
        request_body: anthropic_request("claude-3-7-sonnet-20250219"),
        expected_model: "sonnet-mapped",
        upstream_format: UpstreamFormat::Anthropic,
    })
    .await;
}
