//! HTTP endpoint integration tests.
//!
//! Tests that verify typed HTTP responses against a live opencode server.
//!
//! TODO(3): Add error case tests (invalid session IDs, malformed payloads, missing fields)
//! to verify error responses deserialize correctly.

use super::create_test_client;
use super::should_run;
use opencode_rs::types::message::PromptPart;
use opencode_rs::types::message::PromptRequest;
use opencode_rs::types::message::ShellRequest;
use opencode_rs::types::new_message_id;
use opencode_rs::types::permission::PermissionReply;
use opencode_rs::types::permission::PermissionReplyRequest;
use opencode_rs::types::question::QuestionInfo;
use opencode_rs::types::question::QuestionReply;
use opencode_rs::types::session::SessionInitRequest;
use tokio::time::Duration;

fn answer_for_question(question: &QuestionInfo) -> Vec<String> {
    let text = question.question.to_ascii_lowercase();

    if text.contains("yes") && text.contains("no") {
        return vec!["yes".to_string()];
    }

    if let Some(first_option) = question.options.first() {
        return vec![first_option.label.clone()];
    }

    vec!["yes".to_string()]
}

fn format_error_chain(err: &dyn std::error::Error) -> String {
    use std::fmt::Write;

    let mut output = format!("{err}");
    let mut source = err.source();
    while let Some(s) = source {
        let _ = write!(output, "\n  caused by: {s}");
        source = s.source();
    }
    output
}

async fn drain_questions(
    client: &opencode_rs::Client,
    session_id: &str,
) -> opencode_rs::error::Result<()> {
    let pending = client.question().list().await?;

    for request in pending
        .into_iter()
        .filter(|request| request.session_id == session_id)
    {
        let answers = request.questions.iter().map(answer_for_question).collect();

        client
            .question()
            .reply(&request.id, &QuestionReply { answers })
            .await?;
    }

    Ok(())
}

async fn drain_permissions(
    client: &opencode_rs::Client,
    session_id: &str,
) -> opencode_rs::error::Result<()> {
    let pending = client.permissions().list().await?;

    for request in pending
        .into_iter()
        .filter(|request| request.session_id == session_id)
    {
        client
            .permissions()
            .reply(
                &request.id,
                &PermissionReplyRequest {
                    reply: PermissionReply::Always,
                    message: None,
                },
            )
            .await?;
    }

    Ok(())
}

/// Test session CRUD with typed responses.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_crud_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Create session - returns typed Session
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    assert!(!session.id.is_empty(), "Session should have ID");

    // Get session - returns typed Session
    let fetched = client
        .sessions()
        .get(&session.id)
        .await
        .expect("Failed to get session");

    assert_eq!(fetched.id, session.id, "Session IDs should match");

    // List sessions - returns Vec<Session>
    // Note: Session list may have race conditions with filesystem, so we just verify we can call it
    match client.sessions().list().await {
        Ok(sessions) => {
            println!("Listed {} sessions", sessions.len());
            // Verify structure when sessions exist (can't check specific IDs due to timing)
            if let Some(first) = sessions.first() {
                assert!(!first.id.is_empty(), "Session should have ID");
            }
        }
        Err(e) => {
            // List may fail in some configurations
            println!("List sessions: {e:?}");
        }
    }

    // Delete session
    client
        .sessions()
        .delete(&session.id)
        .await
        .expect("Failed to delete session");
}

/// Test prompt with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_prompt_typed_response() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Create session
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    // Send prompt - returns typed PromptResponse
    let response = client
        .messages()
        .prompt(
            &session.id,
            &PromptRequest {
                parts: vec![PromptPart::Text {
                    text: "Say hello".to_string(),
                    synthetic: None,
                    ignored: None,
                    metadata: None,
                }],
                message_id: None,
                model: None,
                agent: None,
                no_reply: Some(true), // Don't wait for reply
                system: None,
                variant: None,
            },
        )
        .await
        .expect("Failed to send prompt");

    // PromptResponse has typed fields
    // status and message_id are optional but should deserialize
    println!("Prompt response status: {:?}", response.status);
    println!("Prompt response message_id: {:?}", response.message_id);

    // Clean up
    let _ = client.sessions().delete(&session.id).await;
}

/// Test providers list with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_providers_list_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // List providers - returns ProviderListResponse with all/default/connected
    let response = client
        .providers()
        .list()
        .await
        .expect("Failed to list providers");

    // Verify typed fields in the 'all' array
    for provider in &response.all {
        assert!(!provider.id.is_empty(), "Provider should have ID");
        assert!(!provider.name.is_empty(), "Provider should have name");
        println!(
            "Provider: {} ({}) - {:?} models",
            provider.name,
            provider.id,
            provider.models.len()
        );
    }

    // Verify we have proper default and connected data
    println!(
        "Response: {} providers, {} defaults, {} connected",
        response.all.len(),
        response.default.len(),
        response.connected.len()
    );
}

/// Test MCP status with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_mcp_status_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Get MCP status - returns typed McpStatus
    let status = client
        .mcp()
        .status()
        .await
        .expect("Failed to get MCP status");

    // Verify typed fields
    println!("MCP servers: {:?}", status.servers.len());
    for server in &status.servers {
        assert!(!server.name.is_empty(), "MCP server should have name");
        println!("  Server: {} - {:?}", server.name, server.status);
    }
}

/// Test LSP status with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_lsp_status_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Get LSP status - returns Vec<LspServerStatus>
    let servers = client.misc().lsp().await.expect("Failed to get LSP status");

    // Verify we got a response (may be empty if no LSP servers configured)
    println!("LSP servers: {} configured", servers.len());
    for server in &servers {
        println!("  {} ({}): {:?}", server.name, server.id, server.status);
    }
}

/// Test formatter status with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_formatter_status_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Get formatter status - returns Vec<FormatterInfo>
    let formatters = client
        .misc()
        .formatter()
        .await
        .expect("Failed to get formatter status");

    // Verify we got a response (may be empty if no formatters configured)
    println!("Formatters: {} configured", formatters.len());
    for fmt in &formatters {
        println!(
            "  {} - enabled: {}, extensions: {:?}",
            fmt.name, fmt.enabled, fmt.extensions
        );
    }
}

/// Test `OpenAPI` doc with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_openapi_doc_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Get OpenAPI doc - returns typed OpenApiDoc
    let doc = client
        .misc()
        .doc()
        .await
        .expect("Failed to get OpenAPI doc");

    // Verify it's a valid OpenAPI document
    assert!(doc.spec.is_object(), "Doc should be a JSON object");
    assert!(
        doc.spec.get("openapi").is_some() || doc.spec.get("swagger").is_some(),
        "Should be an OpenAPI/Swagger document"
    );
}

/// Test find endpoints with typed responses.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_find_endpoints_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Find text - returns typed FindResponse (uses 'pattern' param)
    match client.find().text("fn").await {
        Ok(text_results) => {
            println!("Text search: got response");
            let _ = text_results.results;
        }
        Err(e) => {
            // May fail if ripgrep not available or no files to search
            println!("Text search not available: {e:?}");
        }
    }

    // Find files - returns typed FindResponse (uses 'query' param)
    match client.find().files("Cargo").await {
        Ok(file_results) => {
            println!("File search: got response");
            let _ = file_results.results;
        }
        Err(e) => {
            println!("File search not available: {e:?}");
        }
    }

    // Find symbols - returns typed FindResponse (currently returns empty)
    match client.find().symbols("main").await {
        Ok(symbol_results) => {
            println!("Symbol search: got response");
            let _ = symbol_results.results;
        }
        Err(e) => {
            println!("Symbol search not available: {e:?}");
        }
    }
}

/// Test message list with typed Part deserialization.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_message_parts_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Create session
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    // Send a prompt
    let _ = client
        .messages()
        .prompt(
            &session.id,
            &PromptRequest {
                parts: vec![PromptPart::Text {
                    text: "Hello".to_string(),
                    synthetic: None,
                    ignored: None,
                    metadata: None,
                }],
                message_id: None,
                model: None,
                agent: None,
                no_reply: Some(true),
                system: None,
                variant: None,
            },
        )
        .await;

    // List messages - should have typed Parts
    let messages = client
        .messages()
        .list(&session.id)
        .await
        .expect("Failed to list messages");

    for message in &messages {
        println!("Message {} has {} parts", message.id(), message.parts.len());
        for part in &message.parts {
            // Parts should deserialize to typed enum variants
            match part {
                opencode_rs::types::Part::Text { text, .. } => {
                    let preview: String = text.chars().take(50).collect();
                    println!("  Text part: {preview}...");
                }
                opencode_rs::types::Part::Tool { tool, state, .. } => {
                    println!(
                        "  Tool part: {} - state: {:?}",
                        tool,
                        state.as_ref().map(opencode_rs::types::ToolState::status)
                    );
                }
                _ => {
                    println!("  Other part type");
                }
            }
        }
    }

    // Clean up
    let _ = client.sessions().delete(&session.id).await;
}

/// Test session init with required body fields.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_init_required_body() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;
    let providers = client.providers().list().await.expect("providers list");

    let mut connected_ids = providers.connected.clone();
    connected_ids.sort();
    connected_ids.dedup();

    let mut selected = None;
    for provider_id in connected_ids {
        let Some(provider) = providers
            .all
            .iter()
            .find(|provider| provider.id == provider_id)
        else {
            continue;
        };

        let model_id = match providers.default.get(&provider_id) {
            Some(default_model_id) if provider.models.contains_key(default_model_id) => {
                default_model_id.clone()
            }
            _ => {
                let mut model_ids: Vec<String> = provider.models.keys().cloned().collect();
                model_ids.sort();
                let Some(first_model_id) = model_ids.into_iter().next() else {
                    continue;
                };
                first_model_id
            }
        };

        selected = Some((provider_id, model_id));
        break;
    }

    let Some((provider_id, model_id)) = selected else {
        println!(
            "Skipping session.init required-body test: no connected providers with usable models (connected={:?})",
            providers.connected
        );
        return;
    };

    println!("session.init test using provider={provider_id} model={model_id}");

    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    let _ = client
        .messages()
        .prompt(
            &session.id,
            &PromptRequest {
                parts: vec![PromptPart::Text {
                    text: "Initialize this session".to_string(),
                    synthetic: None,
                    ignored: None,
                    metadata: None,
                }],
                message_id: None,
                model: None,
                agent: None,
                no_reply: Some(true),
                system: None,
                variant: None,
            },
        )
        .await
        .expect("Failed to create a message for session.init preconditions");

    let session_id = session.id.clone();
    let init_message_id = new_message_id();
    let init_task = tokio::spawn({
        let client = client.clone();
        let session_id = session_id.clone();
        let provider_id = provider_id.clone();
        let model_id = model_id.clone();

        async move {
            client
                .sessions()
                .init(
                    &session_id,
                    &SessionInitRequest {
                        model_id,
                        provider_id,
                        message_id: init_message_id,
                    },
                )
                .await
        }
    });

    let mut init_task = std::pin::pin!(init_task);
    let mut last_step = String::from("spawned session.init task");
    let driver = async {
        // Initial drain in case the server has already raised pending requests by the time we start polling.
        last_step = "initial drain of questions".to_string();
        let _ = drain_questions(&client, &session_id).await;
        last_step = "initial drain of permissions".to_string();
        let _ = drain_permissions(&client, &session_id).await;

        loop {
            tokio::select! {
                join_result = &mut init_task => {
                    last_step = "session.init task completed".to_string();
                    let ok = match join_result {
                        Ok(result) => result?,
                        Err(error) => {
                            return Err(opencode_rs::error::OpencodeError::State(format!(
                                "session.init task join error: {error}"
                            )));
                        }
                    };
                    return Ok::<bool, opencode_rs::error::OpencodeError>(ok);
                }
                () = tokio::time::sleep(Duration::from_millis(50)) => {
                    // This heuristic is intentionally minimal; the test validates init workflow completion, not question semantics.
                    last_step = "polling questions".to_string();
                    drain_questions(&client, &session_id).await?;

                    last_step = "polling permissions".to_string();
                    drain_permissions(&client, &session_id).await?;
                }
            }
        }
    };

    let ok = match tokio::time::timeout(Duration::from_secs(300), driver).await {
        Ok(Ok(ok)) => ok,
        Ok(Err(error)) => {
            init_task.as_mut().abort();
            let _ = client.sessions().delete(&session_id).await;
            panic!(
                "session.init driver should succeed: {}",
                format_error_chain(&error)
            );
        }
        Err(_) => {
            init_task.as_mut().abort();
            let _ = client.sessions().delete(&session_id).await;
            panic!("session.init timed out after 5 minutes; last observed step: {last_step}");
        }
    };

    assert!(ok, "session.init should return true");

    let deleted = client
        .sessions()
        .delete(&session_id)
        .await
        .expect("session.init test cleanup should delete the session");
    assert!(deleted, "session.init cleanup should delete the session");
}

/// Test session diff patch schema.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_diff_patch_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    let diff = client.sessions().diff(&session.id).await;
    println!("session.diff result: {diff:?}");

    let _ = client.sessions().delete(&session.id).await;
}

/// Test shell response shape.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_shell_returns_info_and_parts() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Pick a real agent name from the live server. Current OpenCode versions strictly validate
    // `agent` against the configured agent list; hardcoding upstream defaults
    // (e.g. "build") fails in environments with custom agent definitions.
    let mut agents = client
        .tools()
        .agents()
        .await
        .expect("Failed to list agents");
    agents.sort_by(|a, b| a.name.cmp(&b.name));
    let Some(agent) = agents.into_iter().next() else {
        println!("Skipping shell test: no agents configured on the live server");
        return;
    };
    let agent_name = agent.name;
    println!("shell test using agent={agent_name}");

    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    let shell = client
        .messages()
        .shell(
            &session.id,
            &ShellRequest {
                agent: agent_name,
                command: "echo hello".to_string(),
                message_id: None,
                model: None,
            },
        )
        .await;

    let _ = client.sessions().delete(&session.id).await;

    let shell = shell.expect("Shell call should succeed with upstream-compatible request shape");
    assert!(
        !shell.info.id.is_empty(),
        "shell response should include info.id"
    );
    assert!(
        !shell.parts.is_empty(),
        "shell response should include at least one part"
    );
}

/// Test session with permission ruleset.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_permission_ruleset() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Create session - permission should deserialize as Ruleset if present
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    // Get the session and check permission field
    let fetched = client
        .sessions()
        .get(&session.id)
        .await
        .expect("Failed to get session");

    // Permission may or may not be set
    if let Some(permission) = &fetched.permission {
        println!("Session has {} permission rules", permission.len());
        for rule in permission {
            println!(
                "  Rule: {} {} {:?}",
                rule.permission, rule.pattern, rule.action
            );
        }
    }

    // Clean up
    let _ = client.sessions().delete(&session.id).await;
}
