"""
Comprehensive streaming tool calling tests for all providers.
Tests real-time tool detection in streaming mode with specified models.
"""

import pytest
import os
import json
import time
from typing import List, Dict, Any
from abstractcore import create_llm
from abstractcore.core.types import GenerateResponse


# Test models as specified by user
TEST_MODELS = {
    "ollama": "qwen3:4b-instruct",
    "lmstudio": "qwen/qwen3-4b-2507",
    "mlx": "mlx-community/Qwen3-4B-4bit",
    "huggingface": "Qwen/Qwen3-4B",  # GGUF not supported, using standard model
    "openai": "gpt-5-mini",
    "anthropic": "claude-haiku-4-5",
}

RUN_LIVE = os.getenv("ABSTRACTCORE_RUN_LIVE_API_TESTS") == "1"
RUN_LOCAL = os.getenv("ABSTRACTCORE_RUN_LOCAL_PROVIDER_TESTS") == "1"


class TestStreamToolCalling:
    """Test streaming tool calling across all providers."""

    @pytest.fixture
    def simple_tools(self):
        """Simple tool definitions for testing."""
        return [
            {
                "name": "get_weather",
                "description": "Get current weather for a city",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {
                            "type": "string",
                            "description": "City name"
                        }
                    },
                    "required": ["city"]
                }
            },
            {
                "name": "calculate",
                "description": "Perform a mathematical calculation",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "expression": {
                            "type": "string",
                            "description": "Mathematical expression to evaluate"
                        }
                    },
                    "required": ["expression"]
                }
            }
        ]

    def test_openai_streaming_tools(self, simple_tools):
        """Test OpenAI streaming with tool calling."""
        if not RUN_LIVE:
            pytest.skip("Live API tests disabled (set ABSTRACTCORE_RUN_LIVE_API_TESTS=1)")
        if not os.getenv("OPENAI_API_KEY"):
            pytest.skip("OPENAI_API_KEY not set")

        try:
            provider = create_llm("openai", model=TEST_MODELS["openai"], timeout=30.0)

            # Test streaming with tools
            stream = provider.generate(
                "What's the weather in Paris?",
                tools=simple_tools,
                stream=True
            )

            # Collect chunks and tool calls
            chunks = []
            tool_calls_detected = []
            content_accumulated = ""

            for chunk in stream:
                assert isinstance(chunk, GenerateResponse)
                chunks.append(chunk)

                if chunk.content:
                    content_accumulated += chunk.content

                if chunk.has_tool_calls():
                    tool_calls_detected.extend(chunk.tool_calls)

            # Verify results
            assert len(chunks) > 0, "Should receive chunks"

            # OpenAI should detect tool calls
            if tool_calls_detected:
                print(f"✅ OpenAI detected {len(tool_calls_detected)} tool calls in stream")
                tool_call = tool_calls_detected[0]
                assert tool_call.get('name') in ['get_weather', 'calculate']
                assert 'arguments' in tool_call

                # Parse arguments if string
                args = tool_call.get('arguments')
                if isinstance(args, str) and args.strip():
                    try:
                        args = json.loads(args)
                    except json.JSONDecodeError:
                        # Sometimes arguments come in chunks during streaming
                        args = {}

                if tool_call.get('name') == 'get_weather' and isinstance(args, dict):
                    # Only check arguments if they were properly parsed
                    if 'city' in args:
                        assert args['city']  # Should have a city value
            else:
                # May have responded without tools
                assert len(content_accumulated) > 0, "Should have content if no tools"
                print(f"⚠️ OpenAI responded with text instead of tools: {content_accumulated[:100]}")

        except Exception as e:
            error_msg = str(e).lower()
            if "authentication" in error_msg or "api_key" in error_msg:
                pytest.skip("OpenAI authentication failed")
            elif any(
                keyword in error_msg
                for keyword in (
                    "connection error",
                    "connecterror",
                    "operation not permitted",
                    "network is unreachable",
                    "nodename nor servname provided",
                    "timeout",
                )
            ):
                pytest.skip("OpenAI not reachable in this environment")
            elif "model" in error_msg and "not found" in error_msg:
                pytest.skip(f"Model {TEST_MODELS['openai']} not found - user's claim may be incorrect")
            else:
                raise

    def test_anthropic_streaming_tools(self, simple_tools):
        """Test Anthropic streaming with tool calling."""
        if not RUN_LIVE:
            pytest.skip("Live API tests disabled (set ABSTRACTCORE_RUN_LIVE_API_TESTS=1)")
        if not os.getenv("ANTHROPIC_API_KEY"):
            pytest.skip("ANTHROPIC_API_KEY not set")

        try:
            provider = create_llm("anthropic", model=TEST_MODELS["anthropic"], timeout=30.0)

            # Test streaming with tools
            stream = provider.generate(
                "Calculate 42 multiplied by 17",
                tools=simple_tools,
                stream=True
            )

            # Collect chunks and tool calls
            chunks = []
            tool_calls_detected = []
            content_accumulated = ""

            for chunk in stream:
                assert isinstance(chunk, GenerateResponse)
                chunks.append(chunk)

                if chunk.content:
                    content_accumulated += chunk.content

                if chunk.has_tool_calls():
                    tool_calls_detected.extend(chunk.tool_calls)

            # Verify results
            assert len(chunks) > 0, "Should receive chunks"

            if tool_calls_detected:
                print(f"✅ Anthropic detected {len(tool_calls_detected)} tool calls in stream")
                tool_call = tool_calls_detected[0]
                assert tool_call.get('name') == 'calculate'
                assert 'arguments' in tool_call or 'input' in tool_call
            else:
                # Current implementation may not support streaming tools yet
                print(f"⚠️ Anthropic streaming tool detection not yet implemented")
                assert len(content_accumulated) > 0 or len(chunks) > 0

        except Exception as e:
            error_msg = str(e).lower()
            if "authentication" in error_msg or "api_key" in error_msg:
                pytest.skip("Anthropic authentication failed")
            elif any(
                keyword in error_msg
                for keyword in (
                    "connection error",
                    "connecterror",
                    "operation not permitted",
                    "network is unreachable",
                    "nodename nor servname provided",
                    "timeout",
                )
            ):
                pytest.skip("Anthropic not reachable in this environment")
            elif "model" in error_msg and "not found" in error_msg:
                pytest.skip(f"Model {TEST_MODELS['anthropic']} not found - user's claim may be incorrect")
            else:
                raise

    def test_ollama_streaming_tools(self, simple_tools):
        """Test Ollama streaming with a lightweight model."""
        if not RUN_LOCAL:
            pytest.skip("Local provider tests disabled (set ABSTRACTCORE_RUN_LOCAL_PROVIDER_TESTS=1)")
        try:
            provider = create_llm(
                "ollama",
                model=TEST_MODELS["ollama"],
                base_url="http://localhost:11434",
                timeout=10.0,
            )

            # Test streaming - Ollama models may not support OpenAI-style tools
            # but we can test the streaming works
            stream = provider.generate(
                "What is 15 plus 27? Please calculate.",
                tools=simple_tools,  # Tools provided but may not be used
                stream=True
            )

            chunks = []
            tool_calls_detected = []
            content_accumulated = ""

            for chunk in stream:
                assert isinstance(chunk, GenerateResponse)
                chunks.append(chunk)

                if chunk.content:
                    content_accumulated += chunk.content

                if chunk.has_tool_calls():
                    tool_calls_detected.extend(chunk.tool_calls)

            assert len(chunks) > 0, "Should receive chunks"
            assert len(content_accumulated) > 0 or tool_calls_detected, "Should receive content and/or tool calls"

            print(f"✅ Ollama streaming works, received {len(chunks)} chunks")
            print(f"   Content sample: {content_accumulated[:100]}...")

            # Note: Ollama qwen models may not support OpenAI-style tool calling
            # but the streaming should work

        except Exception as e:
            error_msg = str(e).lower()
            if any(keyword in error_msg for keyword in ["connection", "refused", "timeout"]):
                pytest.skip("Ollama not running")
            elif "model" in error_msg and "not found" in error_msg:
                pytest.skip(f"Model {TEST_MODELS['ollama']} not available - run: ollama pull {TEST_MODELS['ollama']}")
            else:
                raise

    def test_lmstudio_streaming_tools(self, simple_tools):
        """Test LMStudio streaming with a lightweight model."""
        if not RUN_LOCAL:
            pytest.skip("Local provider tests disabled (set ABSTRACTCORE_RUN_LOCAL_PROVIDER_TESTS=1)")
        try:
            provider = create_llm(
                "lmstudio",
                model=TEST_MODELS["lmstudio"],
                base_url="http://localhost:1234/v1",
                timeout=10.0,
            )

            # LMStudio is OpenAI-compatible, so tools might work
            stream = provider.generate(
                "What's the weather like in Tokyo?",
                tools=simple_tools,
                stream=True
            )

            chunks = []
            tool_calls_detected = []
            content_accumulated = ""

            for chunk in stream:
                assert isinstance(chunk, GenerateResponse)
                chunks.append(chunk)

                if chunk.content:
                    content_accumulated += chunk.content

                if chunk.has_tool_calls():
                    tool_calls_detected.extend(chunk.tool_calls)

            assert len(chunks) > 0, "Should receive chunks"

            if tool_calls_detected:
                print(f"✅ LMStudio detected {len(tool_calls_detected)} tool calls")
            else:
                print(f"✅ LMStudio streaming works, content: {content_accumulated[:100]}...")

        except Exception as e:
            error_msg = str(e).lower()
            if any(keyword in error_msg for keyword in ["connection", "refused", "timeout", "operation not permitted"]):
                pytest.skip("LMStudio not running")
            elif "model" in error_msg:
                pytest.skip(f"Model {TEST_MODELS['lmstudio']} not loaded in LMStudio")
            else:
                raise

    def test_mlx_streaming_tools(self, simple_tools):
        """Test MLX streaming with Qwen3-Coder-30B-A3B-Instruct-4bit."""
        if os.getenv("ABSTRACTCORE_RUN_MLX_TESTS") != "1":
            pytest.skip("MLX streaming test is heavy; set ABSTRACTCORE_RUN_MLX_TESTS=1 to run")
        try:
            provider = create_llm("mlx", model=TEST_MODELS["mlx"])

            # MLX simulates streaming, tools unlikely to work
            stream = provider.generate(
                "Calculate the sum of 100 and 200",
                tools=simple_tools,
                stream=True
            )

            chunks = []
            content_accumulated = ""

            for chunk in stream:
                assert isinstance(chunk, GenerateResponse)
                chunks.append(chunk)

                if chunk.content:
                    content_accumulated += chunk.content

            assert len(chunks) > 0, "Should receive chunks"
            assert len(content_accumulated) > 0, "Should accumulate content"

            print(f"✅ MLX streaming simulation works, {len(chunks)} chunks")

        except Exception as e:
            error_msg = str(e).lower()
            if any(keyword in error_msg for keyword in ["mlx", "import", "not found", "failed to load"]):
                pytest.skip("MLX not available or model not found")
            elif "model" in error_msg:
                pytest.skip(f"Model {TEST_MODELS['mlx']} not available")
            else:
                raise

    def test_huggingface_gguf_streaming_tools(self, simple_tools):
        """Test HuggingFace GGUF streaming with unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF.

        Now with full GGUF support via llama-cpp-python!
        """
        if os.getenv("ABSTRACTCORE_RUN_GGUF_TESTS") != "1":
            pytest.skip("GGUF streaming test is opt-in; set ABSTRACTCORE_RUN_GGUF_TESTS=1 to run")
        try:
            # Test GGUF model
            gguf_model = "unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF"
            provider = create_llm("huggingface", model=gguf_model, context_size=2048)

            # Test basic streaming
            stream = provider.generate(
                "What is the capital of France? Answer in one word.",
                stream=True,
                max_tokens=20
            )

            chunks = []
            content_accumulated = ""

            for chunk in stream:
                assert isinstance(chunk, GenerateResponse)
                chunks.append(chunk)

                if chunk.content:
                    content_accumulated += chunk.content

            assert len(chunks) > 0, "Should receive chunks"
            assert len(content_accumulated) > 0, "Should accumulate content"

            print(f"✅ HuggingFace GGUF streaming works, {len(chunks)} chunks")
            print(f"   Content: {content_accumulated[:100]}")

            # Test with tools
            tool_stream = provider.generate(
                "What's the weather in Paris?",
                tools=simple_tools,
                stream=True,
                max_tokens=100
            )

            tool_chunks = []
            tool_calls_detected = []

            for chunk in tool_stream:
                tool_chunks.append(chunk)
                if chunk.has_tool_calls():
                    tool_calls_detected.extend(chunk.tool_calls)

            if tool_calls_detected:
                print(f"✅ GGUF model detected {len(tool_calls_detected)} tool calls")
                for tc in tool_calls_detected[:3]:
                    print(f"   - {tc.get('name')}: {tc.get('arguments')[:50] if tc.get('arguments') else 'no args'}...")
            else:
                print(f"⚠️ GGUF model did not use tools (model capability limitation)")

        except ImportError as e:
            if "llama-cpp-python" in str(e):
                pytest.skip("llama-cpp-python not installed for GGUF support")
            else:
                raise
        except Exception as e:
            error_msg = str(e).lower()
            if "gguf" in error_msg and "not found" in error_msg:
                pytest.skip(f"GGUF model {gguf_model} not available")
            elif "model" in error_msg:
                pytest.skip(f"Model loading error: {str(e)[:100]}")
            else:
                raise

    def test_huggingface_standard_streaming(self, simple_tools):
        """Test HuggingFace with standard transformers model (non-GGUF)."""
        pytest.skip("Skipped - DialoGPT models removed from test suite")

    def test_streaming_performance_comparison(self, simple_tools):
        """Compare streaming performance across available providers."""
        results = {}

        # Test each provider that's available
        providers_to_test = []

        if RUN_LIVE and os.getenv("OPENAI_API_KEY"):
            providers_to_test.append(("openai", TEST_MODELS["openai"], {}))

        if RUN_LIVE and os.getenv("ANTHROPIC_API_KEY"):
            providers_to_test.append(("anthropic", TEST_MODELS["anthropic"], {}))

        if RUN_LOCAL:
            providers_to_test.extend([
                ("ollama", TEST_MODELS["ollama"], {"base_url": "http://localhost:11434", "timeout": 10.0}),
                ("lmstudio", TEST_MODELS["lmstudio"], {"base_url": "http://localhost:1234/v1", "timeout": 10.0}),
            ])

        if not providers_to_test:
            pytest.skip("No providers enabled for performance comparison (set ABSTRACTCORE_RUN_LIVE_API_TESTS=1 and/or ABSTRACTCORE_RUN_LOCAL_PROVIDER_TESTS=1)")

        for provider_name, model, config in providers_to_test:
            try:
                provider = create_llm(provider_name, model=model, **config)

                start = time.time()
                stream = provider.generate(
                    "Count from 1 to 3",
                    stream=True
                )

                first_chunk_time = None
                chunks = []

                for chunk in stream:
                    if first_chunk_time is None:
                        first_chunk_time = time.time() - start
                    chunks.append(chunk)

                total_time = time.time() - start

                results[provider_name] = {
                    "first_chunk_ms": first_chunk_time * 1000 if first_chunk_time else 0,
                    "total_time_ms": total_time * 1000,
                    "chunks": len(chunks),
                    "model": model
                }

            except Exception as e:
                results[provider_name] = {"error": str(e)[:100]}

        # Print performance comparison
        print("\n" + "="*60)
        print("STREAMING PERFORMANCE COMPARISON")
        print("="*60)

        for provider, metrics in results.items():
            if "error" not in metrics:
                print(f"\n{provider.upper()} ({metrics['model']}):")
                print(f"  First chunk: {metrics['first_chunk_ms']:.1f}ms")
                print(f"  Total time: {metrics['total_time_ms']:.1f}ms")
                print(f"  Chunks: {metrics['chunks']}")
                if metrics['chunks'] > 0:
                    print(f"  Avg chunk time: {metrics['total_time_ms']/metrics['chunks']:.1f}ms")
            else:
                print(f"\n{provider.upper()}: Failed - {metrics['error']}")

        assert len(results) > 0, "Should test at least one provider"

    def test_streaming_tool_accumulation(self, simple_tools):
        """Test accumulating tool call arguments across streaming chunks."""
        if not RUN_LIVE:
            pytest.skip("Live API tests disabled (set ABSTRACTCORE_RUN_LIVE_API_TESTS=1)")
        if not os.getenv("OPENAI_API_KEY"):
            pytest.skip("OPENAI_API_KEY not set")

        try:
            provider = create_llm("openai", model=TEST_MODELS["openai"], timeout=30.0)

            stream = provider.generate(
                "Get the weather for Paris and calculate 100 + 200",
                tools=simple_tools,
                stream=True
            )

            # Track how tool calls are accumulated
            tool_call_chunks = []
            accumulated_args = {}

            for chunk in stream:
                if chunk.has_tool_calls():
                    for tc in chunk.tool_calls:
                        tool_call_chunks.append(tc)

                        # Accumulate arguments (they might come in pieces)
                        tc_id = tc.get('id') or tc.get('name')
                        if tc_id not in accumulated_args:
                            accumulated_args[tc_id] = ""

                        args = tc.get('arguments')
                        if args:
                            accumulated_args[tc_id] += args if isinstance(args, str) else json.dumps(args)

            # Verify accumulation worked
            if tool_call_chunks:
                print(f"✅ Received {len(tool_call_chunks)} tool call chunks")
                print(f"   Accumulated {len(accumulated_args)} unique tool calls")

                for tc_id, args_str in accumulated_args.items():
                    try:
                        parsed = json.loads(args_str)
                        print(f"   Tool {tc_id}: {parsed}")
                    except:
                        print(f"   Tool {tc_id}: {args_str[:50]}...")

        except Exception as e:
            if "authentication" in str(e).lower():
                pytest.skip("Authentication failed")
            else:
                # Keep this as a best-effort integration test; model availability can vary.
                pass


if __name__ == "__main__":
    pytest.main([__file__, "-v", "-s"])
