#[cfg(all(test, feature = "native-http"))]
#[ctor::ctor(unsafe)]
fn init_crypto_for_unit_tests() {
    crate::ensure_crypto_provider();
}

#[cfg(test)]
mod serde_tests {
    use crate::types::*;

    #[test]
    fn chat_request_round_trip() {
        let req = ChatCompletionRequest {
            model: "gpt-4".into(),
            messages: vec![
                Message::System(SystemMessage {
                    content: "You are helpful.".into(),
                    name: None,
                }),
                Message::User(UserMessage {
                    content: UserContent::Text("Hello!".into()),
                    name: None,
                }),
            ],
            temperature: Some(0.7),
            max_tokens: Some(100),
            ..Default::default()
        };

        let json = serde_json::to_string(&req).expect("serialization should not fail");
        let parsed: ChatCompletionRequest = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed.model, "gpt-4");
        assert_eq!(parsed.messages.len(), 2);
        assert_eq!(parsed.temperature, Some(0.7));
        assert_eq!(parsed.max_tokens, Some(100));
    }

    #[test]
    fn chat_response_deserialize() {
        let json = r#"{
            "id": "chatcmpl-abc123",
            "object": "chat.completion",
            "created": 1700000000,
            "model": "gpt-4",
            "choices": [{
                "index": 0,
                "message": {
                    "role": "assistant",
                    "content": "Hello!"
                },
                "finish_reason": "stop"
            }],
            "usage": {
                "prompt_tokens": 10,
                "completion_tokens": 5,
                "total_tokens": 15
            }
        }"#;

        let resp: ChatCompletionResponse = serde_json::from_str(json).expect("deserialization should not fail");
        assert_eq!(resp.id, "chatcmpl-abc123");
        assert_eq!(resp.choices.len(), 1);
        assert_eq!(resp.choices[0].message.content.as_deref(), Some("Hello!"));
        assert_eq!(resp.usage.as_ref().expect("usage should be present").total_tokens, 15);
    }

    #[test]
    fn stream_chunk_deserialize() {
        let json = r#"{
            "id": "chatcmpl-abc123",
            "object": "chat.completion.chunk",
            "created": 1700000000,
            "model": "gpt-4",
            "choices": [{
                "index": 0,
                "delta": {
                    "content": "Hello"
                },
                "finish_reason": null
            }]
        }"#;

        let chunk: ChatCompletionChunk = serde_json::from_str(json).expect("deserialization should not fail");
        assert_eq!(chunk.choices[0].delta.content.as_deref(), Some("Hello"));
        assert!(chunk.choices[0].finish_reason.is_none());
    }

    #[test]
    fn tool_call_message_round_trip() {
        let msg = Message::Assistant(AssistantMessage {
            content: None,
            name: None,
            tool_calls: Some(vec![ToolCall {
                id: "call_123".into(),
                call_type: ToolType::Function,
                function: FunctionCall {
                    name: "get_weather".into(),
                    arguments: r#"{"location": "NYC"}"#.into(),
                },
            }]),
            refusal: None,
            function_call: None,
        });

        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");

        if let Message::Assistant(a) = parsed {
            let calls = a.tool_calls.expect("tool_calls should be present");
            assert_eq!(calls.len(), 1);
            assert_eq!(calls[0].function.name, "get_weather");
        } else {
            panic!("expected assistant message");
        }
    }

    #[test]
    fn multipart_content_round_trip() {
        let msg = Message::User(UserMessage {
            content: UserContent::Parts(vec![
                ContentPart::Text {
                    text: "What's in this image?".into(),
                },
                ContentPart::ImageUrl {
                    image_url: ImageUrl {
                        url: "https://example.com/image.png".into(),
                        detail: Some(ImageDetail::High),
                    },
                },
            ]),
            name: None,
        });

        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        assert!(json.contains("image_url"));
        let _: Message = serde_json::from_str(&json).expect("deserialization should not fail");
    }

    #[test]
    fn embedding_request_round_trip() {
        let req = EmbeddingRequest {
            model: "text-embedding-3-small".into(),
            input: EmbeddingInput::Multiple(vec!["hello".into(), "world".into()]),
            encoding_format: None,
            dimensions: Some(256),
            user: None,
        };

        let json = serde_json::to_string(&req).expect("serialization should not fail");
        let parsed: EmbeddingRequest = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed.model, "text-embedding-3-small");
        assert_eq!(parsed.dimensions, Some(256));
    }

    #[test]
    fn embedding_response_deserialize() {
        let json = r#"{
            "object": "list",
            "data": [{
                "object": "embedding",
                "embedding": [0.1, 0.2, 0.3],
                "index": 0
            }],
            "model": "text-embedding-3-small",
            "usage": {
                "prompt_tokens": 5,
                "completion_tokens": 0,
                "total_tokens": 5
            }
        }"#;

        let resp: EmbeddingResponse = serde_json::from_str(json).expect("deserialization should not fail");
        assert_eq!(resp.data.len(), 1);
        assert_eq!(resp.data[0].embedding.len(), 3);
    }

    #[test]
    fn developer_message_round_trip() {
        let msg = Message::Developer(DeveloperMessage {
            content: "You are a dev assistant.".into(),
            name: Some("devbot".into()),
        });
        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        assert!(json.contains("\"role\":\"developer\""));
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");
        if let Message::Developer(d) = parsed {
            assert_eq!(d.content, "You are a dev assistant.");
            assert_eq!(d.name.as_deref(), Some("devbot"));
        } else {
            panic!("expected developer message");
        }
    }

    #[test]
    fn function_message_round_trip() {
        let msg = Message::Function(FunctionMessage {
            content: r#"{"temperature": 72}"#.into(),
            name: "get_weather".into(),
        });
        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        assert!(json.contains("\"role\":\"function\""));
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");
        if let Message::Function(f) = parsed {
            assert_eq!(f.name, "get_weather");
        } else {
            panic!("expected function message");
        }
    }

    #[test]
    fn assistant_message_with_refusal() {
        let msg = Message::Assistant(AssistantMessage {
            content: None,
            name: None,
            tool_calls: None,
            refusal: Some("I cannot help with that.".into()),
            function_call: None,
        });
        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        assert!(json.contains("refusal"));
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");
        if let Message::Assistant(a) = parsed {
            assert_eq!(a.refusal.as_deref(), Some("I cannot help with that."));
        } else {
            panic!("expected assistant message");
        }
    }

    #[test]
    fn finish_reason_function_call_serde() {
        let reason = FinishReason::FunctionCall;
        let json = serde_json::to_string(&reason).expect("serialization should not fail");
        assert_eq!(json, "\"function_call\"");
        let parsed: FinishReason = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, FinishReason::FunctionCall);
    }

    #[test]
    fn service_tier_in_response() {
        let json = r#"{
            "id": "chatcmpl-abc",
            "object": "chat.completion",
            "created": 1700000000,
            "model": "gpt-4",
            "choices": [{
                "index": 0,
                "message": {"role": "assistant", "content": "Hi"},
                "finish_reason": "stop"
            }],
            "service_tier": "default"
        }"#;
        let resp: ChatCompletionResponse = serde_json::from_str(json).expect("deserialization should not fail");
        assert_eq!(resp.service_tier.as_deref(), Some("default"));
    }

    #[test]
    fn response_format_json_schema() {
        let fmt = ResponseFormat::JsonSchema {
            json_schema: JsonSchemaFormat {
                name: "my_schema".into(),
                description: None,
                schema: serde_json::json!({"type": "object"}),
                strict: Some(true),
            },
        };

        let json = serde_json::to_string(&fmt).expect("serialization should not fail");
        assert!(json.contains("json_schema"));
        let _: ResponseFormat = serde_json::from_str(&json).expect("deserialization should not fail");
    }

    #[test]
    fn finish_reason_other_unknown_string() {
        // Catch-all variant for unknown finish reasons from non-OpenAI providers
        let json = r#""custom_stop_reason""#;
        let reason: FinishReason = serde_json::from_str(json).expect("deserialization should not fail");
        assert_eq!(reason, FinishReason::Other);
    }

    #[test]
    fn finish_reason_stop_serde() {
        let reason = FinishReason::Stop;
        let json = serde_json::to_string(&reason).expect("serialization should not fail");
        assert_eq!(json, "\"stop\"");
        let parsed: FinishReason = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, FinishReason::Stop);
    }

    #[test]
    fn finish_reason_length_serde() {
        let reason = FinishReason::Length;
        let json = serde_json::to_string(&reason).expect("serialization should not fail");
        assert_eq!(json, "\"length\"");
        let parsed: FinishReason = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, FinishReason::Length);
    }

    #[test]
    fn embedding_format_float_serde() {
        let fmt = EmbeddingFormat::Float;
        let json = serde_json::to_string(&fmt).expect("serialization should not fail");
        assert_eq!(json, "\"float\"");
        let parsed: EmbeddingFormat = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, EmbeddingFormat::Float);
    }

    #[test]
    fn embedding_format_base64_serde() {
        let fmt = EmbeddingFormat::Base64;
        let json = serde_json::to_string(&fmt).expect("serialization should not fail");
        assert_eq!(json, "\"base64\"");
        let parsed: EmbeddingFormat = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, EmbeddingFormat::Base64);
    }

    #[test]
    fn embedding_input_single_string() {
        let input = EmbeddingInput::Single("hello world".into());
        let json = serde_json::to_string(&input).expect("serialization should not fail");
        assert_eq!(json, "\"hello world\"");
        let parsed: EmbeddingInput = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, input);
    }

    #[test]
    fn embedding_input_multiple_strings() {
        let input = EmbeddingInput::Multiple(vec!["hello".into(), "world".into(), "test".into()]);
        let json = serde_json::to_string(&input).expect("serialization should not fail");
        assert!(json.contains("hello"));
        assert!(json.contains("world"));
        let parsed: EmbeddingInput = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, input);
    }

    #[test]
    fn embedding_request_with_format_and_dimensions() {
        let req = EmbeddingRequest {
            model: "text-embedding-3-large".into(),
            input: EmbeddingInput::Single("test".into()),
            encoding_format: Some(EmbeddingFormat::Base64),
            dimensions: Some(1024),
            user: Some("user-123".into()),
        };

        let json = serde_json::to_string(&req).expect("serialization should not fail");
        let parsed: EmbeddingRequest = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed.encoding_format, Some(EmbeddingFormat::Base64));
        assert_eq!(parsed.dimensions, Some(1024));
        assert_eq!(parsed.user, Some("user-123".into()));
    }

    #[test]
    fn stop_sequence_single_serde() {
        let stop = StopSequence::Single("\\n".into());
        let json = serde_json::to_string(&stop).expect("serialization should not fail");
        assert_eq!(json, "\"\\\\n\"");
        let parsed: StopSequence = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, stop);
    }

    #[test]
    fn stop_sequence_multiple_serde() {
        let stop = StopSequence::Multiple(vec!["\\n".into(), "\\n\\n".into(), "[END]".into()]);
        let json = serde_json::to_string(&stop).expect("serialization should not fail");
        assert!(json.contains("END"));
        let parsed: StopSequence = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, stop);
    }

    #[test]
    fn tool_choice_mode_auto_serde() {
        let choice = ToolChoice::Mode(ToolChoiceMode::Auto);
        let json = serde_json::to_string(&choice).expect("serialization should not fail");
        assert_eq!(json, "\"auto\"");
        let parsed: ToolChoice = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, choice);
    }

    #[test]
    fn tool_choice_mode_required_serde() {
        let choice = ToolChoice::Mode(ToolChoiceMode::Required);
        let json = serde_json::to_string(&choice).expect("serialization should not fail");
        assert_eq!(json, "\"required\"");
        let parsed: ToolChoice = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, choice);
    }

    #[test]
    fn tool_choice_mode_none_serde() {
        let choice = ToolChoice::Mode(ToolChoiceMode::None);
        let json = serde_json::to_string(&choice).expect("serialization should not fail");
        assert_eq!(json, "\"none\"");
        let parsed: ToolChoice = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, choice);
    }

    #[test]
    fn tool_choice_specific_serde() {
        let choice = ToolChoice::Specific(SpecificToolChoice {
            choice_type: ToolType::Function,
            function: SpecificFunction {
                name: "get_weather".into(),
            },
        });
        let json = serde_json::to_string(&choice).expect("serialization should not fail");
        assert!(json.contains("get_weather"));
        let parsed: ToolChoice = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, choice);
    }

    #[test]
    fn response_format_text_serde() {
        let fmt = ResponseFormat::Text;
        let json = serde_json::to_string(&fmt).expect("serialization should not fail");
        assert_eq!(json, "{\"type\":\"text\"}");
        let parsed: ResponseFormat = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, fmt);
    }

    #[test]
    fn response_format_json_object_serde() {
        let fmt = ResponseFormat::JsonObject;
        let json = serde_json::to_string(&fmt).expect("serialization should not fail");
        assert_eq!(json, "{\"type\":\"json_object\"}");
        let parsed: ResponseFormat = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, fmt);
    }

    #[test]
    fn chat_completion_request_default_round_trip() {
        let req = ChatCompletionRequest::default();
        let json = serde_json::to_string(&req).expect("serialization should not fail");
        let parsed: ChatCompletionRequest = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed, req);
        assert!(parsed.model.is_empty());
        assert!(parsed.messages.is_empty());
    }

    #[test]
    fn chat_completion_request_full_fields_partial_eq() {
        let req1 = ChatCompletionRequest {
            model: "gpt-4".into(),
            messages: vec![Message::System(SystemMessage {
                content: "You are helpful.".into(),
                name: None,
            })],
            temperature: Some(0.7),
            max_tokens: Some(100),
            top_p: Some(0.95),
            n: Some(1),
            stream: Some(false),
            stop: Some(StopSequence::Single("\\n".into())),
            presence_penalty: Some(0.0),
            frequency_penalty: Some(0.0),
            logit_bias: None,
            user: Some("user-1".into()),
            tools: None,
            tool_choice: None,
            parallel_tool_calls: None,
            response_format: None,
            stream_options: None,
            seed: None,
            reasoning_effort: None,
            extra_body: None,
        };

        let json = serde_json::to_string(&req1).expect("serialization should not fail");
        let req2: ChatCompletionRequest = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(req1, req2);
    }

    #[test]
    fn message_variant_equality() {
        // Test that Message variants support PartialEq correctly
        let msg1 = Message::System(SystemMessage {
            content: "test".into(),
            name: None,
        });
        let msg2 = Message::System(SystemMessage {
            content: "test".into(),
            name: None,
        });
        assert_eq!(msg1, msg2);

        let msg3 = Message::System(SystemMessage {
            content: "different".into(),
            name: None,
        });
        assert_ne!(msg1, msg3);
    }

    #[test]
    fn message_assistant_round_trip_equality() {
        let msg = Message::Assistant(AssistantMessage {
            content: Some("Hello!".into()),
            name: None,
            tool_calls: None,
            refusal: None,
            function_call: None,
        });
        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(msg, parsed);
    }

    #[test]
    fn message_user_round_trip_equality() {
        let msg = Message::User(UserMessage {
            content: UserContent::Text("What's up?".into()),
            name: None,
        });
        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(msg, parsed);
    }

    #[test]
    fn message_tool_round_trip() {
        let msg = Message::Tool(ToolMessage {
            content: r#"{"result": "sunny"}"#.into(),
            tool_call_id: "call_456".into(),
            name: Some("get_weather".into()),
        });
        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");
        if let Message::Tool(t) = parsed {
            assert_eq!(t.tool_call_id, "call_456");
            assert_eq!(t.name.as_deref(), Some("get_weather"));
        } else {
            panic!("expected tool message");
        }
    }

    #[test]
    fn user_content_parts_image_detail_low() {
        let msg = Message::User(UserMessage {
            content: UserContent::Parts(vec![
                ContentPart::Text {
                    text: "Describe this".into(),
                },
                ContentPart::ImageUrl {
                    image_url: ImageUrl {
                        url: "https://example.com/img.png".into(),
                        detail: Some(ImageDetail::Low),
                    },
                },
            ]),
            name: None,
        });
        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(msg, parsed);
    }

    #[test]
    fn user_content_parts_image_detail_auto() {
        let msg = Message::User(UserMessage {
            content: UserContent::Parts(vec![ContentPart::ImageUrl {
                image_url: ImageUrl {
                    url: "https://example.com/img.png".into(),
                    detail: Some(ImageDetail::Auto),
                },
            }]),
            name: None,
        });
        let json = serde_json::to_string(&msg).expect("serialization should not fail");
        assert!(json.contains("\"detail\":\"auto\""));
        let parsed: Message = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(msg, parsed);
    }

    // -- Search type serialization tests --

    #[test]
    fn search_request_round_trip() {
        use crate::types::search::SearchRequest;
        let req = SearchRequest {
            model: "brave/web-search".into(),
            query: "What is Rust?".into(),
            ..Default::default()
        };
        let json = serde_json::to_string(&req).expect("serialization should not fail");
        let parsed: SearchRequest = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed.model, "brave/web-search");
        assert_eq!(parsed.query, "What is Rust?");
    }

    #[test]
    fn search_request_rejects_unknown_fields() {
        use crate::types::search::SearchRequest;
        let json = r#"{"model":"test","query":"q","unknown_field":true}"#;
        assert!(
            serde_json::from_str::<SearchRequest>(json).is_err(),
            "SearchRequest with deny_unknown_fields should reject unknown keys"
        );
    }

    #[test]
    fn search_request_with_optional_fields() {
        use crate::types::search::SearchRequest;
        let req = SearchRequest {
            model: "tavily/search".into(),
            query: "Rust language".into(),
            max_results: Some(5),
            search_domain_filter: Some(vec!["rust-lang.org".into()]),
            country: Some("US".into()),
        };
        let json = serde_json::to_string(&req).expect("serialization should not fail");
        assert!(json.contains("max_results"));
        assert!(json.contains("rust-lang.org"));
        let parsed: SearchRequest = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed.max_results, Some(5));
        assert_eq!(parsed.country, Some("US".into()));
    }

    #[test]
    fn search_response_deserialize() {
        use crate::types::search::SearchResponse;
        let json = r#"{
            "results": [
                {"title": "Rust", "url": "https://www.rust-lang.org", "snippet": "A language empowering everyone."}
            ],
            "model": "brave/web-search"
        }"#;
        let resp: SearchResponse = serde_json::from_str(json).expect("deserialization should not fail");
        assert_eq!(resp.results.len(), 1);
        assert_eq!(resp.model, "brave/web-search");
        assert_eq!(resp.results[0].title, "Rust");
    }

    // -- OCR type serialization tests --

    #[test]
    fn ocr_request_url_variant() {
        use crate::types::ocr::{OcrDocument, OcrRequest};
        let req = OcrRequest {
            model: "mistral/mistral-ocr-latest".into(),
            document: OcrDocument::Url {
                url: "https://example.com/doc.pdf".into(),
            },
            pages: None,
            include_image_base64: None,
        };
        let json = serde_json::to_string(&req).expect("serialization should not fail");
        let parsed: OcrRequest = serde_json::from_str(&json).expect("deserialization should not fail");
        assert_eq!(parsed.model, "mistral/mistral-ocr-latest");
    }

    #[test]
    fn ocr_document_url_serializes_with_tag() {
        use crate::types::ocr::OcrDocument;
        let doc = OcrDocument::Url {
            url: "https://example.com".into(),
        };
        let json = serde_json::to_string(&doc).expect("serialization should not fail");
        assert!(
            json.contains("document_url"),
            "expected 'document_url' tag in serialized output, got: {json}"
        );
    }

    #[test]
    fn ocr_document_base64_serializes_with_tag() {
        use crate::types::ocr::OcrDocument;
        let doc = OcrDocument::Base64 {
            data: "dGVzdA==".into(),
            media_type: "application/pdf".into(),
        };
        let json = serde_json::to_string(&doc).expect("serialization should not fail");
        assert!(
            json.contains("\"type\":\"base64\""),
            "expected 'base64' tag in serialized output, got: {json}"
        );
        let parsed: OcrDocument = serde_json::from_str(&json).expect("deserialization should not fail");
        if let OcrDocument::Base64 { data, media_type } = parsed {
            assert_eq!(data, "dGVzdA==");
            assert_eq!(media_type, "application/pdf");
        } else {
            panic!("expected OcrDocument::Base64 variant");
        }
    }

    #[test]
    fn ocr_request_rejects_unknown_fields() {
        use crate::types::ocr::OcrRequest;
        let json = r#"{"model":"m","document":{"type":"document_url","url":"u"},"bogus":1}"#;
        assert!(
            serde_json::from_str::<OcrRequest>(json).is_err(),
            "OcrRequest with deny_unknown_fields should reject unknown keys"
        );
    }

    #[test]
    fn ocr_response_deserialize() {
        use crate::types::ocr::OcrResponse;
        let json = r##"{
            "pages": [{"index": 0, "markdown": "# Title"}],
            "model": "mistral/mistral-ocr-latest"
        }"##;
        let resp: OcrResponse = serde_json::from_str(json).expect("deserialization should not fail");
        assert_eq!(resp.pages.len(), 1);
        assert_eq!(resp.pages[0].index, 0);
        assert!(resp.pages[0].markdown.contains("Title"));
        assert_eq!(resp.model, "mistral/mistral-ocr-latest");
    }
}

#[cfg(test)]
mod provider_tests {
    use std::collections::HashMap;

    use crate::provider::{
        AuthConfig, AuthType, ConfigDrivenProvider, OpenAiProvider, Provider, ProviderConfig, detect_provider,
    };

    #[test]
    fn openai_matches() {
        let p = OpenAiProvider;
        assert!(p.matches_model("gpt-4"));
        assert!(p.matches_model("gpt-4o-mini"));
        assert!(p.matches_model("o1-preview"));
        assert!(p.matches_model("o3-mini"));
        assert!(p.matches_model("text-embedding-3-small"));
        assert!(!p.matches_model("claude-3-opus"));
        assert!(!p.matches_model("groq/llama3"));
    }

    #[test]
    fn detect_openai() {
        let p = detect_provider("gpt-4").expect("provider should be detected");
        assert_eq!(p.name(), "openai");
        assert_eq!(p.base_url(), "https://api.openai.com/v1");
    }

    #[test]
    fn detect_groq() {
        let p = detect_provider("groq/llama3-70b").expect("provider should be detected");
        assert_eq!(p.name(), "groq");
        assert_eq!(p.base_url(), "https://api.groq.com/openai/v1");
    }

    #[test]
    fn detect_mistral_prefix() {
        // The registry uses the "mistral/" routing prefix for Mistral models.
        // Bare model names without a slash prefix return None; callers should
        // use the prefixed form "mistral/mistral-large-latest".
        let p = detect_provider("mistral/mistral-large-latest").expect("provider should be detected");
        assert_eq!(p.name(), "mistral");
        assert_eq!(p.base_url(), "https://api.mistral.ai/v1");
    }

    #[test]
    fn detect_ollama() {
        let p = detect_provider("ollama/llama3").expect("provider should be detected");
        assert_eq!(p.name(), "ollama");
        assert_eq!(p.base_url(), "http://localhost:11434/v1");
    }

    #[test]
    fn detect_unknown_returns_none() {
        assert!(detect_provider("some-random-model").is_none());
    }

    fn make_provider(auth_type: AuthType) -> ConfigDrivenProvider {
        // Box::leak gives us a &'static reference, which ConfigDrivenProvider
        // now requires.  Leaking in tests is acceptable — each test process is
        // short-lived and the total number of leaked configs is tiny.
        let cfg: &'static ProviderConfig = Box::leak(Box::new(ProviderConfig {
            name: "test-provider".into(),
            display_name: None,
            base_url: Some("https://api.example.com/v1".into()),
            auth: Some(AuthConfig {
                auth_type,
                env_var: Some("TEST_API_KEY".into()),
            }),
            endpoints: None,
            model_prefixes: None,
            param_mappings: None,
        }));
        ConfigDrivenProvider::new(cfg)
    }

    #[test]
    fn config_driven_bearer_auth() {
        let provider = make_provider(AuthType::Bearer);
        let header = provider.auth_header("my-secret-key");
        assert!(header.is_some());
        let (name, value) = header.expect("auth header should be present");
        assert_eq!(name, "Authorization");
        assert_eq!(value, "Bearer my-secret-key");
    }

    #[test]
    fn config_driven_api_key_auth() {
        let provider = make_provider(AuthType::ApiKey);
        let header = provider.auth_header("my-secret-key");
        assert!(header.is_some());
        let (name, value) = header.expect("auth header should be present");
        assert_eq!(name, "x-api-key");
        assert_eq!(value, "my-secret-key");
    }

    #[test]
    fn config_driven_no_auth() {
        let provider = make_provider(AuthType::None);
        let header = provider.auth_header("my-secret-key");
        assert!(header.is_none(), "AuthType::None should return no auth header");
    }

    #[test]
    fn detect_deepseek() {
        let p = detect_provider("deepseek/deepseek-chat").expect("provider should be detected");
        assert_eq!(p.name(), "deepseek");
    }

    #[test]
    fn detect_cerebras() {
        let p = detect_provider("cerebras/llama-3.1-70b").expect("provider should be detected");
        assert_eq!(p.name(), "cerebras");
    }

    #[test]
    fn detect_openrouter() {
        let p = detect_provider("openrouter/auto").expect("provider should be detected");
        assert_eq!(p.name(), "openrouter");
    }

    #[test]
    fn config_driven_unknown_auth_defaults_to_bearer() {
        let provider = make_provider(AuthType::Unknown);
        let header = provider.auth_header("my-secret-key");
        assert!(header.is_some());
        let (name, value) = header.expect("auth header should be present");
        assert_eq!(name, "Authorization");
        assert!(value.contains("Bearer"));
    }

    #[test]
    fn provider_base_url_contract() {
        let provider = OpenAiProvider;
        assert_eq!(provider.base_url(), "https://api.openai.com/v1");
    }

    #[test]
    fn provider_strip_model_prefix_groq() {
        let p = detect_provider("groq/llama3-70b").expect("provider should be detected");
        let stripped = p.strip_model_prefix("groq/llama3-70b");
        assert_eq!(stripped, "llama3-70b");
    }

    #[test]
    fn provider_strip_model_prefix_openai() {
        let p = OpenAiProvider;
        let stripped = p.strip_model_prefix("gpt-4");
        assert_eq!(stripped, "gpt-4"); // OpenAI has no prefix
    }

    #[test]
    fn detect_anthropic_by_claude_prefix() {
        let p = detect_provider("claude-3-5-sonnet-20241022").expect("provider should be detected");
        assert_eq!(p.name(), "anthropic");
        assert_eq!(p.base_url(), "https://api.anthropic.com/v1");
    }

    #[test]
    fn detect_anthropic_by_slash_prefix() {
        let p = detect_provider("anthropic/claude-3-5-sonnet-20241022").expect("provider should be detected");
        assert_eq!(p.name(), "anthropic");
    }

    #[test]
    fn anthropic_auth_header_uses_x_api_key() {
        let p = detect_provider("claude-3-5-sonnet-20241022").expect("provider should be detected");
        let header = p.auth_header("sk-ant-test");
        assert!(header.is_some());
        let (name, value) = header.expect("auth header should be present");
        assert_eq!(name, "x-api-key");
        assert_eq!(value, "sk-ant-test");
    }

    #[test]
    fn anthropic_extra_headers_contain_version() {
        use crate::provider::anthropic::AnthropicProvider;
        let p = AnthropicProvider;
        let extras = p.extra_headers();
        assert_eq!(extras.len(), 1);
        assert_eq!(extras[0].0, "anthropic-version");
        assert_eq!(extras[0].1, "2023-06-01");
    }

    #[test]
    fn anthropic_strips_prefix() {
        let p = detect_provider("anthropic/claude-3-5-sonnet-20241022").expect("provider should be detected");
        assert_eq!(
            p.strip_model_prefix("anthropic/claude-3-5-sonnet-20241022"),
            "claude-3-5-sonnet-20241022"
        );
    }

    #[test]
    fn anthropic_bare_model_not_stripped() {
        use crate::provider::anthropic::AnthropicProvider;
        let p = AnthropicProvider;
        assert_eq!(
            p.strip_model_prefix("claude-3-5-sonnet-20241022"),
            "claude-3-5-sonnet-20241022"
        );
    }

    #[test]
    fn detect_azure_by_prefix() {
        let p = detect_provider("azure/gpt-4").expect("provider should be detected");
        assert_eq!(p.name(), "azure");
    }

    #[test]
    fn azure_auth_header_uses_api_key() {
        let p = detect_provider("azure/gpt-4").expect("provider should be detected");
        let header = p.auth_header("my-azure-key");
        assert!(header.is_some());
        let (name, value) = header.expect("auth header should be present");
        assert_eq!(name, "api-key");
        assert_eq!(value, "my-azure-key");
    }

    #[test]
    fn azure_strips_prefix() {
        let p = detect_provider("azure/gpt-4").expect("provider should be detected");
        assert_eq!(p.strip_model_prefix("azure/gpt-4"), "gpt-4");
    }

    #[test]
    fn azure_extra_headers_are_empty() {
        use crate::provider::azure::AzureProvider;
        let p = AzureProvider::new();
        assert!(p.extra_headers().is_empty());
    }

    #[test]
    fn detect_vertex_ai_by_prefix() {
        let p = detect_provider("vertex_ai/gemini-2.0-flash").expect("provider should be detected");
        assert_eq!(p.name(), "vertex_ai");
    }

    #[test]
    fn vertex_ai_auth_header_uses_bearer() {
        let p = detect_provider("vertex_ai/gemini-2.0-flash").expect("provider should be detected");
        let header = p.auth_header("ya29.my-access-token");
        assert!(header.is_some(), "Expected an auth header");
        let (name, value) = header.expect("auth header should be present");
        assert_eq!(name, "Authorization");
        assert_eq!(value, "Bearer ya29.my-access-token");
    }

    #[test]
    fn vertex_ai_strips_prefix() {
        let p = detect_provider("vertex_ai/gemini-2.0-flash").expect("provider should be detected");
        assert_eq!(p.strip_model_prefix("vertex_ai/gemini-2.0-flash"), "gemini-2.0-flash");
    }

    #[test]
    fn vertex_ai_bare_model_not_stripped() {
        use crate::provider::vertex::VertexAiProvider;
        let p = VertexAiProvider::new("test-project", "us-central1");
        assert_eq!(p.strip_model_prefix("gemini-2.0-flash"), "gemini-2.0-flash");
    }

    #[test]
    fn vertex_ai_does_not_match_gemini_unprefixed() {
        // Unprefixed "gemini-*" models route to the Google AI Studio (gemini)
        // provider, not Vertex AI.  Vertex AI requires the explicit prefix.
        let p = detect_provider("gemini-2.0-flash");
        // May return Some (from the registry gemini provider) but must NOT be
        // the vertex_ai provider.
        if let Some(p) = p {
            assert_ne!(p.name(), "vertex_ai");
        }
    }

    #[test]
    fn vertex_ai_extra_headers_are_empty() {
        use crate::provider::vertex::VertexAiProvider;
        let p = VertexAiProvider::new("test-project", "us-central1");
        assert!(p.extra_headers().is_empty());
    }

    // ── AWS Bedrock ─────────────────────────────────────────────────────────

    #[test]
    fn detect_bedrock_by_prefix() {
        let p =
            detect_provider("bedrock/anthropic.claude-3-sonnet-20240229-v1:0").expect("provider should be detected");
        assert_eq!(p.name(), "bedrock");
    }

    #[test]
    fn bedrock_matches_only_prefixed_models() {
        use crate::provider::bedrock::BedrockProvider;
        let p = BedrockProvider::from_env();
        assert!(p.matches_model("bedrock/anthropic.claude-3-sonnet-20240229-v1:0"));
        assert!(p.matches_model("bedrock/amazon.nova-pro-v1:0"));
        // Unprefixed model IDs must NOT match — they belong to other providers.
        assert!(!p.matches_model("anthropic.claude-3-sonnet-20240229-v1:0"));
        assert!(!p.matches_model("amazon.nova-pro-v1:0"));
    }

    #[test]
    fn bedrock_strips_prefix() {
        let p =
            detect_provider("bedrock/anthropic.claude-3-sonnet-20240229-v1:0").expect("provider should be detected");
        assert_eq!(
            p.strip_model_prefix("bedrock/anthropic.claude-3-sonnet-20240229-v1:0"),
            "anthropic.claude-3-sonnet-20240229-v1:0"
        );
    }

    #[test]
    fn bedrock_bare_model_not_stripped() {
        use crate::provider::bedrock::BedrockProvider;
        let p = BedrockProvider::from_env();
        // Without the "bedrock/" prefix, the model name is returned unchanged.
        assert_eq!(
            p.strip_model_prefix("anthropic.claude-3-sonnet-20240229-v1:0"),
            "anthropic.claude-3-sonnet-20240229-v1:0"
        );
    }

    #[test]
    fn bedrock_auth_header_returns_none() {
        // SigV4 uses computed headers, not a static auth header.
        let p =
            detect_provider("bedrock/anthropic.claude-3-sonnet-20240229-v1:0").expect("provider should be detected");
        let header = p.auth_header("ignored-for-sigv4");
        assert!(header.is_none(), "BedrockProvider must return None for auth_header");
    }

    #[test]
    fn bedrock_extra_headers_are_empty() {
        use crate::provider::bedrock::BedrockProvider;
        let p = BedrockProvider::from_env();
        assert!(p.extra_headers().is_empty(), "Bedrock has no static extra headers");
    }

    #[test]
    fn bedrock_base_url_includes_region() {
        use crate::provider::bedrock::BedrockProvider;
        let p = BedrockProvider::new("eu-west-1");
        assert_eq!(p.base_url(), "https://bedrock-runtime.eu-west-1.amazonaws.com");
    }

    #[test]
    fn bedrock_default_region_is_us_east_1() {
        use crate::provider::bedrock::BedrockProvider;
        // Ensure AWS_DEFAULT_REGION / AWS_REGION are not set in this test process.
        // We use a direct constructor to guarantee the region regardless of env.
        let p = BedrockProvider::new("us-east-1");
        assert!(p.base_url().contains("us-east-1"));
    }

    #[test]
    fn bedrock_signing_headers_without_feature_returns_empty() {
        // When the `bedrock` feature is disabled, signing_headers() must return
        // an empty vec so requests work against override base-URLs (e.g. mock servers).
        // When the feature IS enabled but credentials are absent, it also returns
        // empty (the error is silently swallowed to avoid panicking).
        use crate::provider::bedrock::BedrockProvider;
        let p = BedrockProvider::new("us-east-1");
        let headers = p.signing_headers("POST", "http://localhost/chat/completions", b"{}");
        // In CI without real AWS credentials, headers will be empty either way.
        // The important invariant: it must not panic.
        let _ = headers;
    }

    fn make_provider_with_mappings(mappings: HashMap<String, String>) -> ConfigDrivenProvider {
        let cfg: &'static ProviderConfig = Box::leak(Box::new(ProviderConfig {
            name: "test-mapped".into(),
            display_name: None,
            base_url: Some("https://api.example.com/v1".into()),
            auth: Some(AuthConfig {
                auth_type: AuthType::Bearer,
                env_var: Some("TEST_API_KEY".into()),
            }),
            endpoints: None,
            model_prefixes: None,
            param_mappings: Some(mappings),
        }));
        ConfigDrivenProvider::new(cfg)
    }

    #[test]
    fn param_mappings_renames_field() {
        let mut mappings = HashMap::new();
        mappings.insert("max_completion_tokens".into(), "max_tokens".into());

        let provider = make_provider_with_mappings(mappings);
        let mut body = serde_json::json!({
            "model": "test/model",
            "messages": [],
            "max_completion_tokens": 512
        });

        provider
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["max_tokens"], 512);
        assert!(body.get("max_completion_tokens").is_none());
    }

    #[test]
    fn param_mappings_skips_absent_field() {
        let mut mappings = HashMap::new();
        mappings.insert("max_completion_tokens".into(), "max_tokens".into());

        let provider = make_provider_with_mappings(mappings);
        let mut body = serde_json::json!({
            "model": "test/model",
            "messages": []
        });

        provider
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert!(body.get("max_tokens").is_none());
        assert!(body.get("max_completion_tokens").is_none());
    }

    #[test]
    fn param_mappings_none_is_noop() {
        let provider = make_provider(AuthType::Bearer);
        let mut body = serde_json::json!({
            "model": "test/model",
            "messages": [],
            "max_completion_tokens": 512
        });

        provider
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        // Field untouched when no mappings configured
        assert_eq!(body["max_completion_tokens"], 512);
    }

    #[test]
    fn param_mappings_multiple_fields() {
        let mut mappings = HashMap::new();
        mappings.insert("max_completion_tokens".into(), "max_tokens".into());
        mappings.insert("frequency_penalty".into(), "repetition_penalty".into());

        let provider = make_provider_with_mappings(mappings);
        let mut body = serde_json::json!({
            "model": "test/model",
            "messages": [],
            "max_completion_tokens": 512,
            "frequency_penalty": 0.5
        });

        provider
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["max_tokens"], 512);
        assert_eq!(body["repetition_penalty"], 0.5);
        assert!(body.get("max_completion_tokens").is_none());
        assert!(body.get("frequency_penalty").is_none());
    }

    #[test]
    fn real_provider_apertis_has_param_mappings() {
        let p = detect_provider("apertis/some-model").expect("provider should be detected");
        let mut body = serde_json::json!({
            "model": "some-model",
            "messages": [{"role": "user", "content": "hi"}],
            "max_completion_tokens": 256
        });

        p.transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["max_tokens"], 256);
        assert!(body.get("max_completion_tokens").is_none());
    }
}

#[cfg(test)]
mod error_tests {
    use crate::error::LiterLlmError;

    #[test]
    fn error_from_401() {
        let err = LiterLlmError::from_status(
            401,
            r#"{"error":{"message":"Invalid API key","type":"invalid_request_error"}}"#,
            None,
        );
        assert!(matches!(err, LiterLlmError::Authentication { .. }));
    }

    #[test]
    fn error_from_429() {
        let err = LiterLlmError::from_status(
            429,
            r#"{"error":{"message":"Rate limited","type":"rate_limit_error"}}"#,
            None,
        );
        assert!(matches!(err, LiterLlmError::RateLimited { .. }));
    }

    #[test]
    fn error_from_context_window() {
        let err = LiterLlmError::from_status(
            400,
            r#"{"error":{"message":"maximum context length exceeded","type":"invalid_request_error"}}"#,
            None,
        );
        assert!(matches!(err, LiterLlmError::ContextWindowExceeded { .. }));
    }

    #[test]
    fn error_from_plain_text() {
        let err = LiterLlmError::from_status(500, "Internal Server Error", None);
        assert!(matches!(err, LiterLlmError::ServerError { .. }));
    }

    #[test]
    fn error_from_503() {
        let err = LiterLlmError::from_status(503, "Service Unavailable", None);
        assert!(matches!(err, LiterLlmError::ServiceUnavailable { .. }));
    }

    #[test]
    fn error_from_403_forbidden() {
        // 403 Forbidden indicates invalid credentials / insufficient
        // permissions — map to Authentication, not ServerError.
        let err = LiterLlmError::from_status(403, "Forbidden", None);
        assert!(matches!(err, LiterLlmError::Authentication { .. }));
    }

    #[test]
    fn error_from_502_bad_gateway() {
        let err = LiterLlmError::from_status(502, "Bad Gateway", None);
        assert!(matches!(err, LiterLlmError::ServiceUnavailable { .. }));
    }

    #[test]
    fn error_from_504_gateway_timeout() {
        let err = LiterLlmError::from_status(504, "Gateway Timeout", None);
        assert!(matches!(err, LiterLlmError::ServiceUnavailable { .. }));
    }

    #[test]
    fn error_from_content_policy_filter() {
        let err = LiterLlmError::from_status(
            400,
            r#"{"error":{"message":"Request violates content_filter policy","type":"invalid_request_error"}}"#,
            None,
        );
        assert!(matches!(err, LiterLlmError::ContentPolicy { .. }));
    }

    #[test]
    fn error_from_content_policy_explicit() {
        let err = LiterLlmError::from_status(
            400,
            r#"{"error":{"message":"content_policy violation detected","type":"invalid_request_error"}}"#,
            None,
        );
        assert!(matches!(err, LiterLlmError::ContentPolicy { .. }));
    }

    #[test]
    fn error_message_preservation() {
        let msg = "Custom error message from provider";
        let err = LiterLlmError::from_status(
            500,
            &format!(r#"{{"error":{{"message":"{}","type":"server_error"}}}}"#, msg),
            None,
        );
        if let LiterLlmError::ServerError { message, .. } = err {
            assert_eq!(message, msg);
        } else {
            panic!("expected ServerError");
        }
    }
}

#[cfg(test)]
mod retry_tests {
    use crate::http::retry::{parse_retry_after, should_retry};

    #[test]
    fn retry_on_429() {
        assert!(should_retry(429, 0, 3, None).is_some());
        assert!(should_retry(429, 1, 3, None).is_some());
        assert!(should_retry(429, 2, 3, None).is_some());
        assert!(should_retry(429, 3, 3, None).is_none()); // exhausted
    }

    #[test]
    fn retry_on_500() {
        assert!(should_retry(500, 0, 3, None).is_some());
        assert!(should_retry(503, 0, 3, None).is_some());
    }

    #[test]
    fn no_retry_on_400() {
        assert!(should_retry(400, 0, 3, None).is_none());
        assert!(should_retry(401, 0, 3, None).is_none());
        assert!(should_retry(404, 0, 3, None).is_none());
    }

    #[test]
    fn no_retry_when_disabled() {
        assert!(should_retry(429, 0, 0, None).is_none());
    }

    #[test]
    fn exponential_backoff() {
        use std::time::Duration;
        // With jitter in [0.5, 1.0] of the base delay, verify each attempt
        // stays within its expected range.  Base delays: 1s, 2s, 4s.
        let d0 = should_retry(429, 0, 3, None).expect("should_retry should return Some for retryable status");
        let d1 = should_retry(429, 1, 3, None).expect("should_retry should return Some for retryable status");
        let d2 = should_retry(429, 2, 3, None).expect("should_retry should return Some for retryable status");
        assert!(d0 >= Duration::from_millis(500) && d0 <= Duration::from_secs(1));
        assert!(d1 >= Duration::from_secs(1) && d1 <= Duration::from_secs(2));
        assert!(d2 >= Duration::from_secs(2) && d2 <= Duration::from_secs(4));
    }

    #[test]
    fn retry_after_header_respected_on_429() {
        use std::time::Duration;
        let server_delay = Duration::from_secs(42);
        let delay = should_retry(429, 0, 3, Some(server_delay)).expect("should retry on 429 with Retry-After");
        assert_eq!(delay, server_delay);
    }

    #[test]
    fn retry_after_header_ignored_on_500() {
        use std::time::Duration;
        // Retry-After is only honoured for 429; on 500 we use exponential backoff.
        let server_delay = Duration::from_secs(42);
        let delay = should_retry(500, 0, 3, Some(server_delay)).expect("should retry on 500");
        // Exponential backoff for attempt 0 = 1 s base, with jitter in [0.5, 1.0].
        assert!(delay >= Duration::from_millis(500) && delay <= Duration::from_secs(1));
    }

    #[test]
    fn parse_retry_after_header() {
        use std::time::Duration;
        assert_eq!(parse_retry_after("30"), Some(Duration::from_secs(30)));
        assert_eq!(parse_retry_after("  5  "), Some(Duration::from_secs(5)));
        assert_eq!(parse_retry_after("not-a-number"), None);
    }

    #[test]
    fn retry_on_504() {
        use std::time::Duration;
        // Jitter scales base delay by [0.5, 1.0], so we check ranges.
        let d0 = should_retry(504, 0, 3, None).expect("should_retry should return Some for retryable status");
        assert!(d0 >= Duration::from_millis(500) && d0 <= Duration::from_secs(1));
        let d1 = should_retry(504, 1, 3, None).expect("should_retry should return Some for retryable status");
        assert!(d1 >= Duration::from_secs(1) && d1 <= Duration::from_secs(2));
        let d2 = should_retry(504, 2, 3, None).expect("should_retry should return Some for retryable status");
        assert!(d2 >= Duration::from_secs(2) && d2 <= Duration::from_secs(4));
    }

    #[test]
    fn retry_on_502() {
        assert!(should_retry(502, 0, 3, None).is_some());
        assert!(should_retry(502, 1, 3, None).is_some());
    }

    #[test]
    fn server_retry_after_capped_at_60s() {
        use std::time::Duration;
        let server_delay = Duration::from_secs(120);
        let delay = should_retry(429, 0, 3, Some(server_delay)).expect("should retry on 429 with Retry-After");
        // Should be capped at 60 seconds
        assert_eq!(delay, Duration::from_secs(60));
    }

    #[test]
    fn server_retry_after_under_cap() {
        use std::time::Duration;
        let server_delay = Duration::from_secs(30);
        let delay = should_retry(429, 0, 3, Some(server_delay)).expect("should retry on 429 with Retry-After");
        // Under 60s cap, should use server value
        assert_eq!(delay, Duration::from_secs(30));
    }

    #[test]
    fn exponential_backoff_caps_at_30s() {
        use std::time::Duration;
        // Attempt 5 would be 2^5 = 32 seconds, capped at 30, then jitter [0.5, 1.0].
        let delay = should_retry(500, 5, 10, None).expect("should_retry should return Some for retryable status");
        assert!(delay >= Duration::from_secs(15) && delay <= Duration::from_secs(30));
    }
}

#[cfg(test)]
mod sse_tests {
    use crate::http::streaming::parse_sse_line;

    #[test]
    fn parse_valid_chunk() {
        let line = r#"data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1700000000,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}]}"#;
        let result = parse_sse_line(line)
            .expect("parse_sse_line should not fail")
            .expect("should yield a chunk");
        assert_eq!(result.choices[0].delta.content.as_deref(), Some("Hi"));
    }

    #[test]
    fn parse_done_returns_none() {
        assert!(parse_sse_line("data: [DONE]").is_none());
    }

    #[test]
    fn parse_non_data_returns_none() {
        assert!(parse_sse_line("event: ping").is_none());
        assert!(parse_sse_line(": comment").is_none());
    }

    #[test]
    fn parse_invalid_json() {
        let result = parse_sse_line("data: {invalid}").expect("parse_sse_line should not fail");
        assert!(result.is_err());
    }

    #[test]
    fn parse_data_without_space() {
        // Some implementations emit "data:{json}" without a trailing space.
        let line = r#"data:{"id":"chatcmpl-123","object":"chat.completion.chunk","created":1700000000,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}]}"#;
        let result = parse_sse_line(line)
            .expect("parse_sse_line should not fail")
            .expect("should yield a chunk");
        assert_eq!(result.choices[0].delta.content.as_deref(), Some("Hi"));
    }

    #[test]
    fn parse_done_without_space() {
        assert!(parse_sse_line("data:[DONE]").is_none());
    }
}

#[cfg(test)]
mod config_tests {
    use crate::client::ClientConfigBuilder;

    #[test]
    fn base_url_sanitization() {
        // Test trailing slash removal
        let builder = ClientConfigBuilder::new("key").base_url("https://api.example.com/");
        let config = builder.build();
        assert_eq!(
            config.base_url.expect("base_url should be present"),
            "https://api.example.com"
        );

        // Test multiple trailing slashes
        let builder = ClientConfigBuilder::new("key").base_url("https://api.example.com///");
        let config = builder.build();
        assert_eq!(
            config.base_url.expect("base_url should be present"),
            "https://api.example.com"
        );

        // Test no trailing slash (remains unchanged)
        let builder = ClientConfigBuilder::new("key").base_url("https://api.example.com");
        let config = builder.build();
        assert_eq!(
            config.base_url.expect("base_url should be present"),
            "https://api.example.com"
        );
    }
}

// ── 2.F tests: capabilities, streaming_format, type-state builder ─────────────

#[cfg(test)]
mod capability_tests {
    use crate::provider::{all_providers, capabilities};

    /// Every provider must return a capabilities struct with all seven boolean
    /// flags accessible.  The fact that registry parsing succeeded (all_providers
    /// returns Ok) confirms the streaming_format field also parsed correctly
    /// (any unknown value would cause a serde error that fails the registry load).
    #[test]
    fn schema_all_providers_have_capabilities_and_streaming_format() {
        let providers = all_providers().expect("registry should load");
        for cfg in providers {
            // capabilities() accesses the internal ProviderEntry which carries
            // the deserialized capabilities and streaming_format fields.
            let caps = capabilities(&cfg.name);
            let _: bool = caps.vision;
            let _: bool = caps.reasoning;
            let _: bool = caps.structured_output;
            let _: bool = caps.function_calling;
            let _: bool = caps.audio_in;
            let _: bool = caps.audio_out;
            let _: bool = caps.video_in;
        }
        assert!(!providers.is_empty(), "registry should have at least one provider");
    }

    /// The total number of providers in the embedded registry must equal 143.
    #[test]
    fn schema_provider_count_is_143() {
        let providers = all_providers().expect("registry should load");
        assert_eq!(
            providers.len(),
            143,
            "expected 143 providers in providers.json, found {}",
            providers.len()
        );
    }

    /// OpenAI must have function_calling = true.
    #[test]
    fn capabilities_openai_function_calling() {
        assert!(
            capabilities("openai").function_calling,
            "openai must advertise function_calling support"
        );
    }

    /// OpenAI must have vision = true (gpt-4o supports image input).
    #[test]
    fn capabilities_openai_vision() {
        assert!(capabilities("openai").vision, "openai must advertise vision support");
    }

    /// Anthropic must have reasoning = true (extended thinking tokens).
    #[test]
    fn capabilities_anthropic_reasoning() {
        assert!(
            capabilities("anthropic").reasoning,
            "anthropic must advertise reasoning support"
        );
    }

    /// Bedrock must have vision = true.
    #[test]
    fn capabilities_bedrock_has_vision() {
        assert!(
            capabilities("bedrock").vision,
            "bedrock must advertise vision support (supported by Claude/Nova models)"
        );
    }

    /// Bedrock entry must exist in the registry.
    #[test]
    fn schema_bedrock_entry_parsed_cleanly() {
        let providers = all_providers().expect("registry should load");
        let bedrock = providers.iter().find(|p| p.name == "bedrock");
        assert!(bedrock.is_some(), "bedrock must be present in registry");
    }

    /// An unknown provider name must return the default (all-false) capabilities.
    #[test]
    fn capabilities_unknown_provider_returns_default() {
        let caps = capabilities("this-provider-does-not-exist");
        assert!(!caps.vision);
        assert!(!caps.reasoning);
        assert!(!caps.structured_output);
        assert!(!caps.function_calling);
        assert!(!caps.audio_in);
        assert!(!caps.audio_out);
        assert!(!caps.video_in);
    }

    /// Providers with only false flags (e.g. vector DBs) return a valid struct.
    #[test]
    fn capabilities_all_false_provider_is_valid() {
        let caps = capabilities("milvus");
        let _ = caps.vision;
        let _ = caps.function_calling;
    }
}

#[cfg(test)]
mod builder_tests {
    // Positive test: ClientBuilder::new().api_key().provider().build() compiles and succeeds.
    //
    // The negative compile-time test (calling .build() without api_key + provider)
    // is enforced by the type-state.  The `build` method is only defined on
    // `ClientBuilder<WithApiKey, WithProvider>`.  Uncommenting any of the
    // following in a test would cause a compile error:
    //
    //   liter_llm::ClientBuilder::new().build();                     // no method `build`
    //   liter_llm::ClientBuilder::new().api_key("k").build();        // no method `build`
    //   liter_llm::ClientBuilder::new().provider("openai").build();  // no method `build`

    use crate::client::builder::{ClientBuilder, NoApiKey, NoProvider, WithApiKey, WithProvider};

    /// Type-state transitions: the builder returns the correct types at each step.
    #[test]
    fn builder_type_state_transitions() {
        let _b: ClientBuilder<NoApiKey, NoProvider> = ClientBuilder::new();
        let _b: ClientBuilder<WithApiKey, NoProvider> = ClientBuilder::new().api_key("sk-test");
        let _b: ClientBuilder<NoApiKey, WithProvider> = ClientBuilder::new().provider("openai");
        let _b: ClientBuilder<WithApiKey, WithProvider> = ClientBuilder::new().api_key("sk-test").provider("openai");
    }

    /// Optional knobs are available at any state and preserve the type state.
    #[test]
    fn builder_optional_knobs_preserve_state() {
        use std::time::Duration;
        let builder = ClientBuilder::new()
            .api_key("sk-test")
            .provider("openai")
            .timeout(Duration::from_secs(30))
            .max_retries(5)
            .load_env(false)
            .base_url("https://api.openai.com/v1");
        let _: ClientBuilder<WithApiKey, WithProvider> = builder;
    }

    /// The default constructor produces a correctly typed builder.
    #[test]
    fn builder_default_produces_no_key_no_provider() {
        let _: ClientBuilder<NoApiKey, NoProvider> = ClientBuilder::default();
    }

    /// build() succeeds with a valid key and provider.
    #[cfg(any(feature = "native-http", feature = "wasm-http"))]
    #[test]
    fn builder_build_succeeds_with_key_and_provider() {
        let result = ClientBuilder::new()
            .api_key("sk-test-key")
            .provider("openai")
            .load_env(false)
            .build();
        assert!(result.is_ok(), "build() should succeed with key + provider");
    }

    /// api_key then provider and provider then api_key both produce WithApiKey+WithProvider.
    #[test]
    fn builder_order_independence() {
        let _: ClientBuilder<WithApiKey, WithProvider> = ClientBuilder::new().api_key("k").provider("openai");
        let _: ClientBuilder<WithApiKey, WithProvider> = ClientBuilder::new().provider("openai").api_key("k");
    }
}

// Regression test: `liter_llm::ContentPart` must resolve to the VLM/chat
// variant (types::ContentPart), not the realtime variant.  Before the fix,
// `realtime::ContentPart` was re-exported at the crate root and shadowed
// `types::ContentPart`, causing E0599 ("no variant named `ImageUrl`") in
// downstream VLM-OCR consumers.
#[cfg(test)]
mod content_part_root_reexport_tests {
    /// Importing `liter_llm::ContentPart` must expose the `ImageUrl` variant.
    ///
    /// This test would fail to *compile* (E0599) if the realtime variant
    /// were still shadowing the types variant at the crate root.
    #[test]
    fn crate_root_content_part_has_image_url_variant() {
        use crate::types::{ContentPart, ImageUrl};

        let part = ContentPart::ImageUrl {
            image_url: ImageUrl {
                url: "https://example.com/image.png".into(),
                detail: None,
            },
        };
        assert!(matches!(part, ContentPart::ImageUrl { .. }));
    }

    /// The realtime variant is still accessible via its full module path.
    #[test]
    fn realtime_content_part_accessible_via_module_path() {
        use crate::realtime::ContentPart as RealtimeContentPart;

        let part = RealtimeContentPart::text("hello");
        assert!(matches!(part, RealtimeContentPart::Text { .. }));
    }
}
