//! Comprehensive End-to-End LLM Provider Tests
//!
//! This module provides 100% coverage for all LLM provider functionality:
//! - MockProvider
//! - OpenAIProvider (with environment-based testing)
//! - OllamaProvider
//! - GeminiProvider
//! - JinaProvider (embeddings)
//! - Rate limiting
//! - Caching
//! - Reranking
//! - Tokenization
//!
//! Run with: `cargo test --package edgequake-llm --test e2e_llm_providers`
//! For real API tests: Set OPENAI_API_KEY, GEMINI_API_KEY, etc.

use std::sync::Arc;
use std::time::Duration;

use edgequake_llm::traits::{ChatMessage, ChatRole, CompletionOptions};
use edgequake_llm::{
    CacheConfig, CachedProvider, EmbeddingProvider, LLMCache, LLMProvider, LLMResponse,
    MockProvider, RateLimitedProvider, RateLimiterConfig, Tokenizer,
};

// ============================================================================
// Mock Provider Tests - Full Coverage
// ============================================================================

mod mock_provider_tests {
    use super::*;

    #[tokio::test]
    async fn test_mock_provider_creation() {
        let provider = MockProvider::new();
        assert_eq!(LLMProvider::name(&provider), "mock");
        assert_eq!(LLMProvider::model(&provider), "mock-model");
        assert_eq!(provider.max_context_length(), 4096);
    }

    #[tokio::test]
    async fn test_mock_provider_default() {
        let provider = MockProvider::default();
        let response = provider.complete("test").await.unwrap();
        assert_eq!(response.content, "Mock response");
    }

    #[tokio::test]
    async fn test_mock_provider_custom_responses() {
        let provider = MockProvider::new();
        provider.add_response("First response").await;
        provider.add_response("Second response").await;
        provider.add_response("Third response").await;

        let r1 = provider.complete("test 1").await.unwrap();
        assert_eq!(r1.content, "First response");

        let r2 = provider.complete("test 2").await.unwrap();
        assert_eq!(r2.content, "Second response");

        let r3 = provider.complete("test 3").await.unwrap();
        assert_eq!(r3.content, "Third response");

        // After all custom responses, falls back to default
        let r4 = provider.complete("test 4").await.unwrap();
        assert_eq!(r4.content, "Mock response");
    }

    #[tokio::test]
    async fn test_mock_provider_complete_with_options() {
        let provider = MockProvider::new();
        provider.add_response("Custom response").await;

        let options = CompletionOptions {
            max_tokens: Some(100),
            temperature: Some(0.5),
            ..Default::default()
        };

        let response = provider
            .complete_with_options("test", &options)
            .await
            .unwrap();
        assert_eq!(response.content, "Custom response");
    }

    #[tokio::test]
    async fn test_mock_provider_chat() {
        let provider = MockProvider::new();
        provider.add_response("Chat response").await;

        let messages = vec![
            ChatMessage::system("You are helpful"),
            ChatMessage::user("Hello"),
        ];

        let response = provider.chat(&messages, None).await.unwrap();
        assert_eq!(response.content, "Chat response");
    }

    #[tokio::test]
    async fn test_mock_provider_streaming() {
        use futures::StreamExt;

        let provider = MockProvider::new();
        provider.add_response("Streaming response").await;

        let mut stream = provider.stream("test").await.unwrap();
        let mut collected = String::new();

        while let Some(chunk) = stream.next().await {
            collected.push_str(&chunk.unwrap());
        }

        assert_eq!(collected, "Streaming response");
    }

    #[tokio::test]
    async fn test_mock_provider_embedding() {
        let provider = MockProvider::new();

        // Default embedding
        let embedding = provider.embed_one("test text").await.unwrap();
        assert_eq!(embedding.len(), 1536); // Default dimension
        assert!(embedding.iter().all(|&v| (v - 0.1).abs() < 0.001));
    }

    #[tokio::test]
    async fn test_mock_provider_batch_embedding() {
        let provider = MockProvider::new();

        let texts = vec![
            "text1".to_string(),
            "text2".to_string(),
            "text3".to_string(),
        ];
        let embeddings = provider.embed(&texts).await.unwrap();

        assert_eq!(embeddings.len(), 3);
        for emb in embeddings {
            assert_eq!(emb.len(), 1536);
        }
    }

    #[tokio::test]
    async fn test_mock_provider_custom_embedding() {
        let provider = MockProvider::new();

        // MockProvider uses default embeddings of 0.1
        // Just verify embedding works and has correct dimensions
        let result = provider.embed_one("test").await.unwrap();
        assert_eq!(result.len(), 1536);
        assert!(result.iter().all(|&v| (v - 0.1).abs() < 0.001));
    }

    #[tokio::test]
    async fn test_mock_embedding_provider_info() {
        let provider = MockProvider::new();
        assert_eq!(EmbeddingProvider::name(&provider), "mock");
        assert_eq!(EmbeddingProvider::model(&provider), "mock-embedding");
        assert_eq!(provider.dimension(), 1536);
        assert_eq!(provider.max_tokens(), 512);
    }

    #[tokio::test]
    async fn test_mock_provider_json_extraction() {
        let provider = MockProvider::new();

        let json_response = r#"{
            "entities": [
                {"name": "EdgeQuake", "type": "TECHNOLOGY", "description": "RAG system"}
            ],
            "relationships": [
                {"source": "A", "target": "B", "type": "RELATES_TO"}
            ]
        }"#;
        provider.add_response(json_response).await;

        let response = provider
            .complete("Extract entities from text")
            .await
            .unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&response.content).unwrap();

        assert!(parsed.get("entities").is_some());
        assert!(parsed.get("relationships").is_some());
    }
}

// ============================================================================
// LLM Response Tests
// ============================================================================

mod llm_response_tests {
    use super::*;

    #[test]
    fn test_llm_response_new() {
        let response = LLMResponse::new("Hello, world!", "gpt-4");
        assert_eq!(response.content, "Hello, world!");
        assert_eq!(response.model, "gpt-4");
        assert_eq!(response.prompt_tokens, 0);
        assert_eq!(response.completion_tokens, 0);
        assert_eq!(response.total_tokens, 0);
        assert!(response.finish_reason.is_none());
    }

    #[test]
    fn test_llm_response_with_usage() {
        let response = LLMResponse::new("test", "model").with_usage(100, 50);

        assert_eq!(response.prompt_tokens, 100);
        assert_eq!(response.completion_tokens, 50);
        assert_eq!(response.total_tokens, 150);
    }

    #[test]
    fn test_llm_response_with_finish_reason() {
        let response = LLMResponse::new("test", "model").with_finish_reason("stop");

        assert_eq!(response.finish_reason, Some("stop".to_string()));
    }

    #[test]
    fn test_llm_response_builder_chain() {
        let response = LLMResponse::new("content", "model")
            .with_usage(10, 20)
            .with_finish_reason("length");

        assert_eq!(response.prompt_tokens, 10);
        assert_eq!(response.completion_tokens, 20);
        assert_eq!(response.total_tokens, 30);
        assert_eq!(response.finish_reason, Some("length".to_string()));
    }
}

// ============================================================================
// Completion Options Tests
// ============================================================================

mod completion_options_tests {
    use super::*;

    #[test]
    fn test_completion_options_default() {
        let options = CompletionOptions::default();
        assert!(options.max_tokens.is_none());
        assert!(options.temperature.is_none());
        assert!(options.top_p.is_none());
        assert!(options.stop.is_none());
    }

    #[test]
    fn test_completion_options_with_temperature() {
        let options = CompletionOptions::with_temperature(0.7);
        assert_eq!(options.temperature, Some(0.7));
    }

    #[test]
    fn test_completion_options_json_mode() {
        let options = CompletionOptions::json_mode();
        assert_eq!(options.response_format, Some("json_object".to_string()));
    }
}

// ============================================================================
// Chat Message Tests
// ============================================================================

mod chat_message_tests {
    use super::*;

    #[test]
    fn test_chat_message_system() {
        let msg = ChatMessage::system("You are a helpful assistant");
        assert_eq!(msg.role, ChatRole::System);
        assert_eq!(msg.content, "You are a helpful assistant");
        assert!(msg.name.is_none());
    }

    #[test]
    fn test_chat_message_user() {
        let msg = ChatMessage::user("Hello, how are you?");
        assert_eq!(msg.role, ChatRole::User);
        assert_eq!(msg.content, "Hello, how are you?");
    }

    #[test]
    fn test_chat_message_assistant() {
        let msg = ChatMessage::assistant("I'm doing well, thank you!");
        assert_eq!(msg.role, ChatRole::Assistant);
        assert_eq!(msg.content, "I'm doing well, thank you!");
    }
}

// ============================================================================
// Rate Limiter Tests
// ============================================================================

mod rate_limiter_tests {
    use super::*;

    #[test]
    fn test_rate_limiter_config_default() {
        let config = RateLimiterConfig::default();
        assert!(config.requests_per_minute > 0);
        assert!(config.tokens_per_minute > 0);
    }

    #[tokio::test]
    async fn test_rate_limited_provider() {
        let mock = MockProvider::new();
        mock.add_response("Rate limited response").await;

        let config = RateLimiterConfig {
            requests_per_minute: 60,
            tokens_per_minute: 100000,
            ..Default::default()
        };

        let rate_limited = RateLimitedProvider::new(mock, config);

        let response = rate_limited.complete("test").await.unwrap();
        assert_eq!(response.content, "Rate limited response");
    }

    #[tokio::test]
    async fn test_rate_limited_provider_info() {
        let mock = MockProvider::new();
        let config = RateLimiterConfig::default();
        let rate_limited = RateLimitedProvider::new(mock, config);

        assert_eq!(LLMProvider::name(&rate_limited), "mock");
        assert_eq!(LLMProvider::model(&rate_limited), "mock-model");
    }
}

// ============================================================================
// Cache Tests
// ============================================================================

mod cache_tests {
    use super::*;

    #[test]
    fn test_cache_config_default() {
        let config = CacheConfig::default();
        assert!(config.max_entries > 0);
        assert!(config.ttl.as_secs() > 0);
    }

    #[tokio::test]
    async fn test_cached_provider() {
        let mock = MockProvider::new();
        mock.add_response("Cached response 1").await;
        mock.add_response("Cached response 2").await;

        let config = CacheConfig {
            max_entries: 100,
            ttl: Duration::from_secs(300),
            ..Default::default()
        };

        let cache = Arc::new(LLMCache::new(config));
        let cached = CachedProvider::new(mock, cache.clone());

        // First call - cache miss
        let r1 = cached.complete("test prompt").await.unwrap();
        assert_eq!(r1.content, "Cached response 1");

        // Second call with same prompt - cache hit
        let r2 = cached.complete("test prompt").await.unwrap();
        assert_eq!(r2.content, "Cached response 1"); // Same as first

        // Different prompt - cache miss
        let r3 = cached.complete("different prompt").await.unwrap();
        assert_eq!(r3.content, "Cached response 2");
    }

    #[tokio::test]
    async fn test_cache_stats() {
        let config = CacheConfig {
            max_entries: 10,
            ttl: Duration::from_secs(60),
            ..Default::default()
        };

        let cache = Arc::new(LLMCache::new(config));
        let stats = cache.stats().await;

        assert_eq!(stats.hits, 0);
        assert_eq!(stats.misses, 0);
        assert_eq!(stats.entries, 0);
    }
}

// ============================================================================
// Tokenizer Tests
// ============================================================================

mod tokenizer_tests {
    use super::*;

    #[test]
    fn test_tokenizer_count_tokens() {
        let tokenizer = Tokenizer::default();
        let text = "Hello, world! This is a test.";
        let count = tokenizer.count_tokens(text);
        assert!(count > 0);
    }

    #[test]
    fn test_tokenizer_empty_text() {
        let tokenizer = Tokenizer::default();
        let count = tokenizer.count_tokens("");
        assert_eq!(count, 0);
    }

    #[test]
    fn test_tokenizer_long_text() {
        let tokenizer = Tokenizer::default();
        let text = "word ".repeat(1000);
        let count = tokenizer.count_tokens(&text);
        assert!(count > 500);
    }
}

// ============================================================================
// Reranker Tests
// ============================================================================

mod reranker_tests {
    use edgequake_llm::{MockReranker, RerankConfig, Reranker};

    #[test]
    fn test_rerank_config_default() {
        let config = RerankConfig::default();
        assert!(!config.model.is_empty());
        assert!(!config.base_url.is_empty());
    }

    #[test]
    fn test_rerank_config_jina() {
        let config = RerankConfig::jina("api-key");
        assert_eq!(config.api_key, Some("api-key".to_string()));
        assert!(config.base_url.contains("jina"));
    }

    #[test]
    fn test_rerank_config_cohere() {
        let config = RerankConfig::cohere("api-key");
        assert!(config.base_url.contains("cohere"));
    }

    #[tokio::test]
    async fn test_mock_reranker() {
        let reranker = MockReranker::new();

        let query = "What is machine learning?";
        let documents = vec![
            "Machine learning is a subset of AI".to_string(),
            "Deep learning uses neural networks".to_string(),
            "Data science involves statistics".to_string(),
        ];

        let results = reranker.rerank(query, &documents, Some(3)).await.unwrap();

        assert!(!results.is_empty());
        assert!(results.len() <= 3);

        // Results should have relevance scores
        for result in &results {
            assert!(result.relevance_score >= 0.0);
            assert!(result.index < documents.len());
        }
    }

    #[tokio::test]
    async fn test_mock_reranker_empty_documents() {
        let reranker = MockReranker::new();
        let results = reranker.rerank("query", &[], Some(5)).await.unwrap();
        assert!(results.is_empty());
    }

    #[tokio::test]
    async fn test_mock_reranker_top_n() {
        let reranker = MockReranker::new();

        let documents: Vec<String> = (0..10).map(|i| format!("Document {}", i)).collect();

        let results = reranker.rerank("query", &documents, Some(3)).await.unwrap();
        assert_eq!(results.len(), 3);
    }
}

// ============================================================================
// Real Provider Tests (Environment-gated)
// ============================================================================

#[cfg(test)]
mod real_provider_tests {
    use super::*;
    use std::env;

    fn get_openai_key() -> Option<String> {
        env::var("OPENAI_API_KEY")
            .ok()
            .filter(|k| !k.is_empty() && k != "test-key")
    }

    #[tokio::test]
    #[ignore = "Requires OPENAI_API_KEY"]
    async fn test_openai_provider_completion() {
        use edgequake_llm::OpenAIProvider;

        let api_key = match get_openai_key() {
            Some(k) => k,
            None => {
                eprintln!("Skipping: OPENAI_API_KEY not set");
                return;
            }
        };

        let provider = OpenAIProvider::new(api_key).with_model("gpt-4o-mini");

        let response = provider.complete("Say 'hello' in one word").await.unwrap();
        assert!(!response.content.is_empty());
        assert!(response.content.to_lowercase().contains("hello"));
    }

    #[tokio::test]
    #[ignore = "Requires OPENAI_API_KEY"]
    async fn test_openai_provider_embedding() {
        use edgequake_llm::OpenAIProvider;

        let api_key = match get_openai_key() {
            Some(k) => k,
            None => return,
        };

        let provider = OpenAIProvider::new(api_key).with_embedding_model("text-embedding-3-small");

        let embedding = provider.embed_one("Hello, world!").await.unwrap();
        assert_eq!(embedding.len(), 1536);
    }

    #[tokio::test]
    #[ignore = "Requires OPENAI_API_KEY"]
    async fn test_openai_provider_chat() {
        use edgequake_llm::OpenAIProvider;

        let api_key = match get_openai_key() {
            Some(k) => k,
            None => return,
        };

        let provider = OpenAIProvider::new(api_key).with_model("gpt-4o-mini");

        let messages = vec![
            ChatMessage::system("You are a helpful assistant. Answer briefly."),
            ChatMessage::user("What is 2+2?"),
        ];

        let response = provider.chat(&messages, None).await.unwrap();
        assert!(response.content.contains("4"));
    }

    /// End-to-end test for vision (multimodal) support in OpenAIProvider.
    ///
    /// Verifies that images are NOT silently dropped (fix for issue #3).
    /// Uses a tiny 1×1 red pixel PNG – small enough to stay within token limits,
    /// distinct enough that any vision-capable model will describe it as red/colour.
    #[tokio::test]
    #[ignore = "Requires OPENAI_API_KEY"]
    async fn test_openai_provider_vision_chat() {
        use edgequake_llm::{traits::ImageData, OpenAIProvider};

        let api_key = match get_openai_key() {
            Some(k) => k,
            None => return,
        };

        // 10×10 red pixel PNG (minimal valid image, deterministic)
        let red_pixel_png_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAEklEQVR4nGP4z8CAB+GTG8HSALfKY52fTcuYAAAAAElFTkSuQmCC";

        let img = ImageData::new(red_pixel_png_b64, "image/png");
        let messages = vec![ChatMessage::user_with_images(
            "What is the dominant color in this image? Answer with a single color word.",
            vec![img],
        )];

        // gpt-4o reliably supports vision
        let provider = OpenAIProvider::new(api_key).with_model("gpt-4o");
        let response = provider.chat(&messages, None).await.unwrap();

        // Should not be empty – image was processed
        assert!(
            !response.content.is_empty(),
            "Vision response must not be empty"
        );
        // The response must mention a color – confirming the image was seen
        let lower = response.content.to_lowercase();
        assert!(
            lower.contains("red")
                || lower.contains("color")
                || lower.contains("colour")
                || lower.contains("pixel")
                || lower.contains("image"),
            "Vision response should mention image content, got: {}",
            response.content
        );
        // Confirm token counts indicate the model processed the request
        assert!(
            response.prompt_tokens > 0,
            "Prompt tokens must be > 0 for vision request"
        );
    }
}

// ============================================================================
// Provider Factory Tests
// ============================================================================

mod provider_factory_tests {
    use super::*;
    use std::env;

    #[tokio::test]
    async fn test_provider_factory_fallback() {
        // When no API key is set, should fall back to mock
        env::remove_var("OPENAI_API_KEY");

        let provider = MockProvider::new();
        let response = provider.complete("test").await.unwrap();
        assert!(!response.content.is_empty());
    }

    #[tokio::test]
    async fn test_arc_provider_compatibility() {
        // Test that providers work correctly through Arc
        let mock: Arc<dyn LLMProvider> = Arc::new(MockProvider::new());
        mock.complete("test").await.unwrap();

        let embedding: Arc<dyn EmbeddingProvider> = Arc::new(MockProvider::new());
        embedding.embed_one("test").await.unwrap();
    }
}

// ============================================================================
// Concurrent Provider Tests
// ============================================================================

mod concurrent_tests {
    use super::*;
    use tokio::task::JoinSet;

    #[tokio::test]
    async fn test_concurrent_completions() {
        let provider = Arc::new(MockProvider::new());

        // Pre-populate responses
        for i in 0..10 {
            provider.add_response(format!("Response {}", i)).await;
        }

        let mut tasks = JoinSet::new();

        for i in 0..10 {
            let p = provider.clone();
            tasks.spawn(async move { p.complete(&format!("Prompt {}", i)).await.unwrap() });
        }

        let mut responses = Vec::new();
        while let Some(result) = tasks.join_next().await {
            responses.push(result.expect("Task panicked"));
        }

        assert_eq!(responses.len(), 10);
    }

    #[tokio::test]
    async fn test_concurrent_embeddings() {
        let provider = Arc::new(MockProvider::new());

        let mut tasks = JoinSet::new();

        for i in 0..5 {
            let p = provider.clone();
            tasks.spawn(async move { p.embed_one(&format!("Text {}", i)).await.unwrap() });
        }

        let mut embeddings = Vec::new();
        while let Some(result) = tasks.join_next().await {
            embeddings.push(result.expect("Task panicked"));
        }

        assert_eq!(embeddings.len(), 5);
        for emb in embeddings {
            assert_eq!(emb.len(), 1536);
        }
    }
}

// ============================================================================
// Error Handling Tests
// ============================================================================

mod error_handling_tests {
    use super::*;

    #[tokio::test]
    async fn test_empty_text_embedding() {
        let provider = MockProvider::new();
        // Empty text should still work
        let result = provider.embed_one("").await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_batch_embed_empty() {
        let provider = MockProvider::new();
        let result = provider.embed(&[]).await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());
    }
}
