//! Unit tests for the Anthropic provider.
use super::AnthropicProvider;
use super::types::{AnthropicContent, AnthropicContentBlock};
use crate::llm::types::{
    FunctionCall, Message, Role, ToolCall, ToolDefinition, ToolFunctionDefinition,
};
use serde_json::json;

fn user_msg(content: &str) -> Message {
    Message {
        role: Role::User,
        content: content.to_string(),
        tool_calls: None,
        tool_call_id: None,
    }
}

fn system_msg(content: &str) -> Message {
    Message {
        role: Role::System,
        content: content.to_string(),
        tool_calls: None,
        tool_call_id: None,
    }
}

fn assistant_msg(content: &str) -> Message {
    Message {
        role: Role::Assistant,
        content: content.to_string(),
        tool_calls: None,
        tool_call_id: None,
    }
}

fn make_tool_def(name: &str) -> ToolDefinition {
    ToolDefinition {
        kind: "function".to_string(),
        function: ToolFunctionDefinition {
            name: name.to_string(),
            description: format!("{name} description"),
            parameters: json!({"type": "object", "properties": {}}),
        },
    }
}

// ── convert_tools ────────────────────────────────────────────────────────

#[test]
fn test_convert_tools_empty() {
    let result = AnthropicProvider::convert_tools(&[]);
    assert!(result.is_empty());
}

#[test]
fn test_convert_tools_single() {
    let tool = make_tool_def("search_files");
    let result = AnthropicProvider::convert_tools(&[tool]);
    assert_eq!(result.len(), 1);
    assert_eq!(result[0].name, "search_files");
    assert_eq!(result[0].description, "search_files description");
    assert_eq!(
        result[0].input_schema,
        json!({"type": "object", "properties": {}})
    );
}

#[test]
fn test_convert_tools_multiple() {
    let tools = vec![make_tool_def("read_file"), make_tool_def("write_file")];
    let result = AnthropicProvider::convert_tools(&tools);
    assert_eq!(result.len(), 2);
    assert_eq!(result[0].name, "read_file");
    assert_eq!(result[1].name, "write_file");
}

// ── convert_messages ─────────────────────────────────────────────────────

#[test]
fn test_convert_messages_user() {
    let msgs = vec![user_msg("Hello")];
    let result = AnthropicProvider::convert_messages(&msgs);
    assert_eq!(result.len(), 1);
    assert_eq!(result[0].role, "user");
    match &result[0].content {
        AnthropicContent::Text(t) => assert_eq!(t, "Hello"),
        _ => panic!("Expected text content"),
    }
}

#[test]
fn test_convert_messages_system_skipped() {
    let msgs = vec![system_msg("You are helpful"), user_msg("Hi")];
    let result = AnthropicProvider::convert_messages(&msgs);
    assert_eq!(result.len(), 1);
    assert_eq!(result[0].role, "user");
}

#[test]
fn test_convert_messages_assistant_text() {
    let msgs = vec![assistant_msg("I can help with that")];
    let result = AnthropicProvider::convert_messages(&msgs);
    assert_eq!(result.len(), 1);
    assert_eq!(result[0].role, "assistant");
    match &result[0].content {
        AnthropicContent::Text(t) => assert_eq!(t, "I can help with that"),
        _ => panic!("Expected text content"),
    }
}

#[test]
fn test_convert_messages_assistant_with_tool_calls_no_content() {
    let msgs = vec![Message {
        role: Role::Assistant,
        content: String::new(),
        tool_calls: Some(vec![ToolCall {
            id: Some("toolu_abc".to_string()),
            kind: "function".to_string(),
            function: FunctionCall {
                name: "search_files".to_string(),
                arguments: r#"{"pattern": "*.rs"}"#.to_string(),
            },
        }]),
        tool_call_id: None,
    }];
    let result = AnthropicProvider::convert_messages(&msgs);
    assert_eq!(result.len(), 1);
    assert_eq!(result[0].role, "assistant");
    match &result[0].content {
        AnthropicContent::Blocks(blocks) => {
            assert_eq!(blocks.len(), 1);
            match &blocks[0] {
                AnthropicContentBlock::ToolUse { id, name, .. } => {
                    assert_eq!(id, "toolu_abc");
                    assert_eq!(name, "search_files");
                }
                _ => panic!("Expected ToolUse block"),
            }
        }
        _ => panic!("Expected blocks content"),
    }
}

#[test]
fn test_convert_messages_assistant_with_tool_calls_and_content() {
    let msgs = vec![Message {
        role: Role::Assistant,
        content: "Let me search for that".to_string(),
        tool_calls: Some(vec![ToolCall {
            id: Some("toolu_xyz".to_string()),
            kind: "function".to_string(),
            function: FunctionCall {
                name: "read_file".to_string(),
                arguments: r#"{"path": "main.rs"}"#.to_string(),
            },
        }]),
        tool_call_id: None,
    }];
    let result = AnthropicProvider::convert_messages(&msgs);
    assert_eq!(result.len(), 1);
    match &result[0].content {
        AnthropicContent::Blocks(blocks) => {
            assert_eq!(blocks.len(), 2);
            match &blocks[0] {
                AnthropicContentBlock::Text { text } => {
                    assert_eq!(text, "Let me search for that");
                }
                _ => panic!("Expected Text block first"),
            }
            match &blocks[1] {
                AnthropicContentBlock::ToolUse { id, name, .. } => {
                    assert_eq!(id, "toolu_xyz");
                    assert_eq!(name, "read_file");
                }
                _ => panic!("Expected ToolUse block second"),
            }
        }
        _ => panic!("Expected blocks content"),
    }
}

#[test]
fn test_convert_messages_assistant_tool_call_invalid_json_args() {
    // Invalid JSON args fall back to {}
    let msgs = vec![Message {
        role: Role::Assistant,
        content: String::new(),
        tool_calls: Some(vec![ToolCall {
            id: None,
            kind: "function".to_string(),
            function: FunctionCall {
                name: "my_tool".to_string(),
                arguments: "not valid json".to_string(),
            },
        }]),
        tool_call_id: None,
    }];
    let result = AnthropicProvider::convert_messages(&msgs);
    match &result[0].content {
        AnthropicContent::Blocks(blocks) => match &blocks[0] {
            AnthropicContentBlock::ToolUse { input, .. } => {
                assert_eq!(*input, json!({}));
            }
            _ => panic!("Expected ToolUse block"),
        },
        _ => panic!("Expected blocks"),
    }
}

#[test]
fn test_convert_messages_assistant_tool_call_no_id_generates_uuid() {
    let msgs = vec![Message {
        role: Role::Assistant,
        content: String::new(),
        tool_calls: Some(vec![ToolCall {
            id: None, // no id provided
            kind: "function".to_string(),
            function: FunctionCall {
                name: "my_tool".to_string(),
                arguments: "{}".to_string(),
            },
        }]),
        tool_call_id: None,
    }];
    let result = AnthropicProvider::convert_messages(&msgs);
    match &result[0].content {
        AnthropicContent::Blocks(blocks) => match &blocks[0] {
            AnthropicContentBlock::ToolUse { id, .. } => {
                assert!(!id.is_empty(), "Should have generated a UUID");
            }
            _ => panic!("Expected ToolUse block"),
        },
        _ => panic!("Expected blocks"),
    }
}

#[test]
fn test_convert_messages_tool_role() {
    let msgs = vec![Message {
        role: Role::Tool,
        content: "file content here".to_string(),
        tool_calls: None,
        tool_call_id: Some("toolu_123".to_string()),
    }];
    let result = AnthropicProvider::convert_messages(&msgs);
    assert_eq!(result.len(), 1);
    assert_eq!(result[0].role, "user");
    match &result[0].content {
        AnthropicContent::Blocks(blocks) => {
            assert_eq!(blocks.len(), 1);
            match &blocks[0] {
                AnthropicContentBlock::ToolResult {
                    tool_use_id,
                    content,
                } => {
                    assert_eq!(tool_use_id, "toolu_123");
                    assert_eq!(content, "file content here");
                }
                _ => panic!("Expected ToolResult block"),
            }
        }
        _ => panic!("Expected blocks content"),
    }
}

#[test]
fn test_convert_messages_tool_role_no_id_defaults_empty() {
    let msgs = vec![Message {
        role: Role::Tool,
        content: "result".to_string(),
        tool_calls: None,
        tool_call_id: None,
    }];
    let result = AnthropicProvider::convert_messages(&msgs);
    match &result[0].content {
        AnthropicContent::Blocks(blocks) => match &blocks[0] {
            AnthropicContentBlock::ToolResult { tool_use_id, .. } => {
                assert_eq!(tool_use_id, "");
            }
            _ => panic!("Expected ToolResult block"),
        },
        _ => panic!("Expected blocks"),
    }
}

#[test]
fn test_convert_messages_mixed_roles() {
    let msgs = vec![
        system_msg("Be helpful"),
        user_msg("What is the time?"),
        assistant_msg("I can check that."),
    ];
    let result = AnthropicProvider::convert_messages(&msgs);
    // System is skipped
    assert_eq!(result.len(), 2);
    assert_eq!(result[0].role, "user");
    assert_eq!(result[1].role, "assistant");
}

// ── extract_system_prompt ─────────────────────────────────────────────────

#[test]
fn test_extract_system_prompt_no_messages() {
    let msgs: Vec<Message> = vec![];
    let prompt = AnthropicProvider::extract_system_prompt(&msgs);
    assert!(prompt.is_empty());
}

#[test]
fn test_extract_system_prompt_no_system_messages() {
    let msgs = vec![user_msg("Hello"), assistant_msg("Hi there")];
    let prompt = AnthropicProvider::extract_system_prompt(&msgs);
    assert!(prompt.is_empty());
}

#[test]
fn test_extract_system_prompt_single() {
    let msgs = vec![system_msg("You are a helpful assistant"), user_msg("Hi")];
    let prompt = AnthropicProvider::extract_system_prompt(&msgs);
    assert_eq!(prompt, "You are a helpful assistant");
}

#[test]
fn test_extract_system_prompt_multiple_joined() {
    let msgs = vec![
        system_msg("First instruction"),
        system_msg("Second instruction"),
        user_msg("Hello"),
    ];
    let prompt = AnthropicProvider::extract_system_prompt(&msgs);
    assert_eq!(prompt, "First instruction\n\nSecond instruction");
}

// ── chat_stream (HTTP mocked) ─────────────────────────────────────────────

#[tokio::test]
async fn test_chat_stream_text_response() {
    let mut server = mockito::Server::new_async().await;

    let delta1 = json!({
        "type": "content_block_delta",
        "delta": {"type": "text_delta", "text": "Hello"}
    });
    let delta2 = json!({
        "type": "content_block_delta",
        "delta": {"type": "text_delta", "text": " world"}
    });
    let body = format!("data: {delta1}\ndata: {delta2}\ndata: [DONE]\n");

    let _m = server
        .mock("POST", "/v1/messages")
        .with_status(200)
        .with_header("content-type", "text/event-stream")
        .with_body(body)
        .create_async()
        .await;

    let provider = AnthropicProvider::new_with_url(
        "test-key".to_string(),
        format!("{}/v1/messages", server.url()),
    );
    let (_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
    let mut tokens = Vec::<String>::new();

    let result = provider
        .chat_stream(
            "claude-3-5-sonnet-20241022",
            &[user_msg("Hello")],
            &[],
            &mut cancel_rx,
            |t| tokens.push(t.to_string()),
            |_| {},
        )
        .await;

    assert!(result.is_ok());
    let response = result.unwrap();
    assert_eq!(response.content, Some("Hello world".to_string()));
    assert!(response.tool_calls.is_none());
    assert_eq!(tokens, vec!["Hello", " world"]);
}

#[tokio::test]
async fn test_chat_stream_error_response() {
    let mut server = mockito::Server::new_async().await;

    let _m = server
        .mock("POST", "/v1/messages")
        .with_status(401)
        .with_body(r#"{"error":{"type":"authentication_error","message":"Invalid API key"}}"#)
        .create_async()
        .await;

    let provider = AnthropicProvider::new_with_url(
        "bad-key".to_string(),
        format!("{}/v1/messages", server.url()),
    );
    let (_tx, mut cancel_rx) = tokio::sync::watch::channel(false);

    let result = provider
        .chat_stream(
            "claude-3-5-sonnet-20241022",
            &[user_msg("Hello")],
            &[],
            &mut cancel_rx,
            |_| {},
            |_| {},
        )
        .await;

    assert!(result.is_err());
    assert!(result.unwrap_err().contains("401"));
}

#[tokio::test]
async fn test_chat_stream_tool_use_response() {
    let mut server = mockito::Server::new_async().await;

    let start_event = json!({
        "type": "content_block_start",
        "content_block": {"type": "tool_use", "id": "toolu_abc", "name": "search_files"}
    });
    let delta_event = json!({
        "type": "content_block_delta",
        "delta": {"type": "input_json_delta", "partial_json": "{}"}
    });
    let stop_event = json!({"type": "content_block_stop"});
    let body =
        format!("data: {start_event}\ndata: {delta_event}\ndata: {stop_event}\ndata: [DONE]\n");

    let _m = server
        .mock("POST", "/v1/messages")
        .with_status(200)
        .with_header("content-type", "text/event-stream")
        .with_body(body)
        .create_async()
        .await;

    let provider = AnthropicProvider::new_with_url(
        "test-key".to_string(),
        format!("{}/v1/messages", server.url()),
    );
    let (_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
    let mut activities = Vec::<String>::new();

    let result = provider
        .chat_stream(
            "claude-3-5-sonnet-20241022",
            &[user_msg("Find Rust files")],
            &[make_tool_def("search_files")],
            &mut cancel_rx,
            |_| {},
            |a| activities.push(a.to_string()),
        )
        .await;

    assert!(result.is_ok());
    let response = result.unwrap();
    assert!(response.content.is_none());
    let tool_calls = response.tool_calls.expect("Expected tool calls");
    assert_eq!(tool_calls.len(), 1);
    assert_eq!(tool_calls[0].id, Some("toolu_abc".to_string()));
    assert_eq!(tool_calls[0].function.name, "search_files");
    assert_eq!(activities, vec!["search_files"]);
}

#[tokio::test]
async fn test_chat_stream_includes_system_prompt() {
    let mut server = mockito::Server::new_async().await;

    let delta = json!({
        "type": "content_block_delta",
        "delta": {"type": "text_delta", "text": "ok"}
    });
    let body = format!("data: {delta}\ndata: [DONE]\n");

    let _m = server
        .mock("POST", "/v1/messages")
        .with_status(200)
        .with_header("content-type", "text/event-stream")
        .with_body(body)
        .create_async()
        .await;

    let provider = AnthropicProvider::new_with_url(
        "test-key".to_string(),
        format!("{}/v1/messages", server.url()),
    );
    let (_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
    let messages = vec![system_msg("Be concise"), user_msg("Hello")];

    let result = provider
        .chat_stream(
            "claude-3-5-sonnet-20241022",
            &messages,
            &[],
            &mut cancel_rx,
            |_| {},
            |_| {},
        )
        .await;

    assert!(result.is_ok());
    assert_eq!(result.unwrap().content, Some("ok".to_string()));
}

#[tokio::test]
async fn test_chat_stream_empty_response_gives_none_content() {
    let mut server = mockito::Server::new_async().await;

    let _m = server
        .mock("POST", "/v1/messages")
        .with_status(200)
        .with_header("content-type", "text/event-stream")
        .with_body("data: [DONE]\n")
        .create_async()
        .await;

    let provider = AnthropicProvider::new_with_url(
        "test-key".to_string(),
        format!("{}/v1/messages", server.url()),
    );
    let (_tx, mut cancel_rx) = tokio::sync::watch::channel(false);

    let result = provider
        .chat_stream(
            "claude-3-5-sonnet-20241022",
            &[user_msg("Hello")],
            &[],
            &mut cancel_rx,
            |_| {},
            |_| {},
        )
        .await;

    assert!(result.is_ok());
    let response = result.unwrap();
    assert!(response.content.is_none());
    assert!(response.tool_calls.is_none());
}
