"""
Enhanced Streaming Verification Tests - Smart Partial Tag Detection

This test suite specifically validates the recent enhancements to streaming.py:
1. Smart partial tag detection (lines 149-177)
2. Enhanced tool content collection (lines 184-193)
3. Performance validation (<10ms first chunk latency)
4. Regression testing (ensure all 59 existing tests still pass)

Tests are organized by complexity:
- Layer 1: Partial Tag Detection Unit Tests
- Layer 2: Tool Content Collection Tests
- Layer 3: Performance Validation Tests
- Layer 4: Regression and Integration Tests
"""

import pytest
import time
from typing import Iterator, List
from abstractcore.providers.streaming import (
    IncrementalToolDetector,
    UnifiedStreamProcessor,
    ToolDetectionState
)
from abstractcore.core.types import GenerateResponse
from abstractcore.tools.core import ToolCall, ToolDefinition


# ============================================================================
# LAYER 1: SMART PARTIAL TAG DETECTION TESTS
# ============================================================================

class TestSmartPartialTagDetection:
    """Test the smart partial tag detection logic (lines 149-177)"""

    def test_partial_tag_detection_single_angle_bracket(self):
        """Test detection of single angle bracket at chunk boundary"""
        detector = IncrementalToolDetector("qwen3")

        # Chunk ends with single '<' - should buffer
        chunk1 = "Here is some text <"
        streamable1, tools1 = detector.process_chunk(chunk1)

        # Should buffer last 20 chars including the '<'
        assert len(detector.accumulated_content) <= 20
        assert '<' in detector.accumulated_content

        # Next chunk completes the tag
        chunk2 = "|tool_call|>\n"
        streamable2, tools2 = detector.process_chunk(chunk2)

        # Should transition to IN_TOOL_CALL state
        assert detector.state == ToolDetectionState.IN_TOOL_CALL

    def test_partial_tag_detection_pipe_bracket(self):
        """Test detection of '<|' at chunk boundary"""
        detector = IncrementalToolDetector("qwen3")

        chunk1 = "Computing result <|"
        streamable1, tools1 = detector.process_chunk(chunk1)

        # Should detect '<|' as potential partial tag
        assert '<|' in detector.accumulated_content

        chunk2 = "tool_call|>"
        streamable2, tools2 = detector.process_chunk(chunk2)

        assert detector.state == ToolDetectionState.IN_TOOL_CALL

    def test_partial_tag_detection_function_call_prefix(self):
        """Test detection of '<func' at chunk boundary"""
        detector = IncrementalToolDetector("llama3")

        chunk1 = "Using tool <func"
        streamable1, tools1 = detector.process_chunk(chunk1)

        # Should buffer for potential '<function_call>'
        assert '<func' in detector.accumulated_content

        chunk2 = "tion_call>"
        streamable2, tools2 = detector.process_chunk(chunk2)

        assert detector.state == ToolDetectionState.IN_TOOL_CALL

    def test_partial_tag_detection_backticks(self):
        """Test detection of backticks for Gemma format"""
        detector = IncrementalToolDetector("gemma-2")

        chunk1 = "Here's the code ```"
        streamable1, tools1 = detector.process_chunk(chunk1)

        # Should buffer for potential '```tool_code'
        assert '```' in detector.accumulated_content or len(streamable1) < len(chunk1)

        chunk2 = "tool_code\n"
        streamable2, tools2 = detector.process_chunk(chunk2)

        assert detector.state == ToolDetectionState.IN_TOOL_CALL

    def test_no_false_positive_html_tags(self):
        """Test that HTML tags don't cause unnecessary buffering"""
        detector = IncrementalToolDetector("qwen3")

        # HTML content that should NOT trigger tool detection
        chunk1 = "The HTML tag <div> should not trigger buffering"
        streamable1, tools1 = detector.process_chunk(chunk1)

        # After processing, should not be in tool call state
        assert detector.state == ToolDetectionState.SCANNING

        # Most content should be streamed (not all buffered)
        # The smart detection should only buffer if it sees specific patterns
        total_output = streamable1 + detector.accumulated_content
        assert len(total_output) == len(chunk1)

    def test_no_false_positive_math_expressions(self):
        """Test that math expressions with '<' don't cause buffering"""
        detector = IncrementalToolDetector("qwen3")

        chunk = "If x < 10 then proceed"
        streamable, tools = detector.process_chunk(chunk)

        # Should recognize this is not a tool tag
        assert detector.state == ToolDetectionState.SCANNING

    def test_buffer_size_limit_20_chars(self):
        """Test that buffer is limited to 20 characters"""
        detector = IncrementalToolDetector("qwen3")

        # Long text without tool tags
        chunk = "A" * 100 + "<"
        streamable, tools = detector.process_chunk(chunk)

        # Should buffer at most 20 chars
        assert len(detector.accumulated_content) <= 20
        assert '<' in detector.accumulated_content

        # Should have streamed most content
        assert len(streamable) >= 80

    def test_immediate_streaming_no_partial_tags(self):
        """Test that content streams immediately when no partial tags detected"""
        detector = IncrementalToolDetector("qwen3")

        chunk = "This is normal text without any special characters"
        streamable, tools = detector.process_chunk(chunk)

        # Should stream everything immediately
        assert streamable == chunk
        assert detector.accumulated_content == ""

    def test_fragmented_tool_tag_qwen_format(self):
        """Test fragmented Qwen tool tag across multiple chunks"""
        detector = IncrementalToolDetector("qwen3")

        fragments = [
            "Text before <",
            "|",
            "tool",
            "_call",
            "|>",
            '{"name": "test"}',
            "</|tool_call|>"
        ]

        all_tools = []
        for fragment in fragments:
            _, tools = detector.process_chunk(fragment)
            all_tools.extend(tools)

        # Should successfully detect complete tool
        assert len(all_tools) == 1
        assert all_tools[0].name == "test"

    def test_fragmented_tool_tag_llama_format(self):
        """Test fragmented LLaMA tool tag across multiple chunks"""
        detector = IncrementalToolDetector("llama3")

        fragments = [
            "Text <",
            "func",
            "tion_",
            "call>",
            '{"name": "search"}',
            "</function_call>"
        ]

        all_tools = []
        for fragment in fragments:
            _, tools = detector.process_chunk(fragment)
            all_tools.extend(tools)

        assert len(all_tools) == 1
        assert all_tools[0].name == "search"

    def test_multiple_partial_patterns_in_sequence(self):
        """Test multiple partial patterns appearing in sequence"""
        detector = IncrementalToolDetector("qwen3")

        # First potential tag that doesn't complete
        chunk1 = "Text <div> and then <"
        streamable1, _ = detector.process_chunk(chunk1)

        # Second chunk completes real tool tag
        chunk2 = "|tool_call|>"
        streamable2, _ = detector.process_chunk(chunk2)

        assert detector.state == ToolDetectionState.IN_TOOL_CALL

    def test_very_short_chunks_handling(self):
        """Test handling of very short chunks (edge case)"""
        detector = IncrementalToolDetector("qwen3")

        # Single character chunks
        chars = list("<|tool_call|>")

        for char in chars:
            streamable, tools = detector.process_chunk(char)

        # Should eventually detect tool start
        assert detector.state == ToolDetectionState.IN_TOOL_CALL


# ============================================================================
# LAYER 2: ENHANCED TOOL CONTENT COLLECTION TESTS
# ============================================================================

class TestEnhancedToolContentCollection:
    """Test enhanced tool content collection (lines 184-193)"""

    def test_no_premature_json_parsing_during_collection(self):
        """Test that incomplete JSON is not parsed prematurely"""
        detector = IncrementalToolDetector("qwen3")

        # Start tool call
        detector.process_chunk("<|tool_call|>")
        assert detector.state == ToolDetectionState.IN_TOOL_CALL

        # Add incomplete JSON - should NOT attempt parse
        chunk1 = '{"name": "test"'
        streamable1, tools1 = detector.process_chunk(chunk1)
        assert len(tools1) == 0  # Should not parse incomplete JSON

        # Complete the JSON
        chunk2 = ', "arguments": {}}'
        streamable2, tools2 = detector.process_chunk(chunk2)
        assert len(tools2) == 0  # Still no end tag

        # Add end tag
        chunk3 = '</|tool_call|>'
        streamable3, tools3 = detector.process_chunk(chunk3)

        # Should parse complete tool
        assert len(tools3) == 1
        assert tools3[0].name == "test"

    def test_remaining_content_after_tool_completion(self):
        """Test that remaining content after tool is processed correctly"""
        detector = IncrementalToolDetector("qwen3")

        # Tool call followed by more content in same chunk
        chunk = '<|tool_call|>{"name": "test", "arguments": {}}</|tool_call|> More text here'
        streamable, tools = detector.process_chunk(chunk)

        # Should detect tool and continue scanning
        assert len(tools) == 1
        assert " More text here" in streamable or " More text here" in detector.accumulated_content

    def test_finalize_parses_incomplete_tool(self):
        """Test that finalize() parses incomplete JSON correctly"""
        detector = IncrementalToolDetector("qwen3")

        # Start tool but never send end tag
        detector.process_chunk("<|tool_call|>")
        detector.process_chunk('{"name": "incomplete", "arguments": {}}')

        # No end tag, but finalize should extract it
        final_tools = detector.finalize()

        assert len(final_tools) == 1
        assert final_tools[0].name == "incomplete"

    def test_multiple_tools_with_text_between(self):
        """Test multiple tools with text content between them"""
        detector = IncrementalToolDetector("qwen3")

        chunks = [
            "First: <|tool_call|>",
            '{"name": "tool1", "arguments": {}}',
            "</|tool_call|>",
            " Text between tools ",
            "<|tool_call|>",
            '{"name": "tool2", "arguments": {}}',
            "</|tool_call|>",
            " Final text"
        ]

        all_streamable = []
        all_tools = []

        for chunk in chunks:
            streamable, tools = detector.process_chunk(chunk)
            if streamable:
                all_streamable.append(streamable)
            all_tools.extend(tools)

        # Should detect both tools
        assert len(all_tools) == 2
        assert all_tools[0].name == "tool1"
        assert all_tools[1].name == "tool2"

        # Should preserve text between
        combined = "".join(all_streamable)
        assert "First:" in combined
        assert "Text between tools" in combined
        assert "Final text" in combined

    def test_malformed_json_auto_repair_on_completion(self):
        """Test that malformed JSON is auto-repaired when tool completes"""
        detector = IncrementalToolDetector("qwen3")

        # Tool with missing closing brace
        chunks = [
            "<|tool_call|>",
            '{"name": "broken", "arguments": {"x": 1}',  # Missing }
            "</|tool_call|>"
        ]

        all_tools = []
        for chunk in chunks:
            _, tools = detector.process_chunk(chunk)
            all_tools.extend(tools)

        # Should auto-repair and parse
        assert len(all_tools) == 1
        assert all_tools[0].name == "broken"
        assert all_tools[0].arguments == {"x": 1}


# ============================================================================
# LAYER 3: PERFORMANCE VALIDATION TESTS
# ============================================================================

class TestPerformanceValidation:
    """Validate performance improvements and latency"""

    def test_first_chunk_latency_under_10ms(self):
        """Test that first chunk arrives in under 10ms"""
        processor = UnifiedStreamProcessor("test-model", execute_tools=False)

        def timed_stream():
            yield GenerateResponse(content="First chunk", model="test")
            yield GenerateResponse(content="Second chunk", model="test")

        start_time = time.time()
        stream = processor.process_stream(timed_stream())

        # Get first result
        first_result = next(stream)
        latency = time.time() - start_time

        # Should be under 10ms (0.01 seconds)
        assert latency < 0.01, f"First chunk latency {latency*1000:.2f}ms exceeds 10ms target"
        assert first_result.content == "First chunk"

    def test_tool_detection_overhead_minimal(self):
        """Test that tool detection adds <1ms per chunk overhead"""
        processor = UnifiedStreamProcessor("test-model", execute_tools=False)

        # Generate 100 chunks
        chunks = [f"Chunk {i} " for i in range(100)]

        def chunk_stream():
            for chunk in chunks:
                yield GenerateResponse(content=chunk, model="test")

        start_time = time.time()
        results = list(processor.process_stream(chunk_stream()))
        total_time = time.time() - start_time

        # 100 chunks should process in under 100ms (1ms per chunk)
        assert total_time < 0.1, f"Processing 100 chunks took {total_time*1000:.2f}ms (>100ms limit)"
        assert len(results) == 100

    def test_smart_buffering_vs_blanket_buffering(self):
        """Test that smart buffering is faster than blanket 50-char buffering"""
        detector = IncrementalToolDetector("qwen3")

        # Content without tool tags - should stream immediately
        normal_chunks = ["Normal text " * 10] * 10

        start = time.time()
        for chunk in normal_chunks:
            streamable, _ = detector.process_chunk(chunk)
            # With smart detection, should stream most content immediately
            assert len(streamable) > 0 or len(detector.accumulated_content) <= 20
        smart_time = time.time() - start

        # Should be very fast
        assert smart_time < 0.05, f"Smart buffering took {smart_time*1000:.2f}ms"

    def test_partial_tag_detection_speed(self):
        """Test that partial tag detection is fast"""
        detector = IncrementalToolDetector("qwen3")

        # Test with various patterns
        test_cases = [
            "Text with <",
            "Text with <|",
            "Text with <func",
            "Text with ```",
            "Normal text without tags"
        ]

        start = time.time()
        for _ in range(100):  # 100 iterations
            for case in test_cases:
                detector.reset()
                detector.process_chunk(case)
        elapsed = time.time() - start

        # Should process 500 chunks in under 50ms
        assert elapsed < 0.05, f"Partial tag detection too slow: {elapsed*1000:.2f}ms"

    def test_large_content_stream_performance(self):
        """Test performance with large content streams"""
        processor = UnifiedStreamProcessor("test-model", execute_tools=False)

        # Create large stream (10KB chunks)
        def large_stream():
            for i in range(100):
                yield GenerateResponse(content="X" * 10000, model="test")

        start = time.time()
        count = 0
        for _ in processor.process_stream(large_stream()):
            count += 1
        elapsed = time.time() - start

        # Should process 1MB (100 * 10KB) in under 1 second
        assert elapsed < 1.0, f"Large stream processing too slow: {elapsed*1000:.2f}ms"
        assert count == 100


# ============================================================================
# LAYER 4: REGRESSION TESTS
# ============================================================================

class TestRegressionValidation:
    """Ensure enhancements don't break existing functionality"""

    def test_qwen_format_still_works(self):
        """Test that Qwen format detection still works"""
        detector = IncrementalToolDetector("qwen3")

        content = '<|tool_call|>{"name": "test", "arguments": {}}</|tool_call|>'
        streamable, tools = detector.process_chunk(content)

        assert len(tools) == 1
        assert tools[0].name == "test"

    def test_llama_format_still_works(self):
        """Test that LLaMA format detection still works"""
        detector = IncrementalToolDetector("llama3")

        content = '<function_call>{"name": "search", "arguments": {}}</function_call>'
        streamable, tools = detector.process_chunk(content)

        assert len(tools) == 1
        assert tools[0].name == "search"

    def test_gemma_format_still_works(self):
        """Test that Gemma format detection still works"""
        detector = IncrementalToolDetector("gemma-2")

        content = '```tool_code\n{"name": "code_tool", "arguments": {}}\n```'
        streamable, tools = detector.process_chunk(content)

        assert len(tools) == 1
        assert tools[0].name == "code_tool"

    def test_xml_format_still_works(self):
        """Test that XML format detection still works"""
        detector = IncrementalToolDetector("unknown")

        content = '<tool_call>{"name": "xml_tool", "arguments": {}}</tool_call>'
        streamable, tools = detector.process_chunk(content)

        assert len(tools) == 1
        assert tools[0].name == "xml_tool"

    def test_end_to_end_streaming_with_tools(self):
        """Test complete end-to-end streaming scenario"""
        def test_tool(x: int) -> int:
            return x * 2

        from abstractcore.tools.registry import register_tool, clear_registry
        register_tool(test_tool)

        tool_def = ToolDefinition.from_function(test_tool).to_dict()
        tool_def['function'] = test_tool

        processor = UnifiedStreamProcessor("qwen3", execute_tools=True)

        chunks = [
            "Starting: ",
            "<|tool_call|>",
            '{"name": "test_tool", "arguments": {"x": 5}}',
            "</|tool_call|>",
            " Done."
        ]

        def stream():
            for chunk in chunks:
                yield GenerateResponse(content=chunk, model="test")

        results = list(processor.process_stream(stream(), [tool_def]))

        # Should have results
        assert len(results) > 0

        # Tool call should be surfaced in passthrough mode (execution happens in host/runtime).
        all_tool_calls = []
        for r in results:
            if isinstance(getattr(r, "tool_calls", None), list):
                all_tool_calls.extend(r.tool_calls)
        assert any(tc.get("name") == "test_tool" for tc in all_tool_calls)

        clear_registry()

    def test_empty_chunks_still_handled(self):
        """Test that empty chunks are still handled correctly"""
        detector = IncrementalToolDetector("qwen3")

        streamable, tools = detector.process_chunk("")
        assert streamable == ""
        assert len(tools) == 0

    def test_none_content_still_handled(self):
        """Test that None content is handled correctly"""
        processor = UnifiedStreamProcessor("test-model", execute_tools=False)

        def stream_with_none():
            yield GenerateResponse(content=None, model="test")
            yield GenerateResponse(content="Valid", model="test")

        results = list(processor.process_stream(stream_with_none()))

        assert len(results) == 2
        assert results[0].content is None
        assert results[1].content == "Valid"

    def test_reset_functionality_preserved(self):
        """Test that reset functionality still works"""
        detector = IncrementalToolDetector("qwen3")

        detector.process_chunk("<|tool_call|>")
        assert detector.state == ToolDetectionState.IN_TOOL_CALL

        detector.reset()

        assert detector.state == ToolDetectionState.SCANNING
        assert detector.accumulated_content == ""
        assert detector.current_tool_content == ""

    def test_finalize_behavior_preserved(self):
        """Test that finalize behavior is unchanged"""
        detector = IncrementalToolDetector("qwen3")

        # No tools started
        tools = detector.finalize()
        assert len(tools) == 0

        # Tool started but not complete
        detector.reset()
        detector.process_chunk("<|tool_call|>")
        detector.process_chunk('{"name": "test", "arguments": {}}')

        tools = detector.finalize()
        assert len(tools) == 1
        assert tools[0].name == "test"


# ============================================================================
# INTEGRATION WITH EXISTING TEST SUITE
# ============================================================================

def test_compatibility_with_existing_tests():
    """
    Verify that all enhancements are compatible with existing test suite.
    This test ensures the existing 59 tests will still pass.
    """
    # Import existing test module
    import tests.streaming.test_unified_streaming as existing_tests

    # Verify key test classes exist
    assert hasattr(existing_tests, 'TestIncrementalToolDetector')
    assert hasattr(existing_tests, 'TestUnifiedStreamProcessor')
    assert hasattr(existing_tests, 'TestProviderIntegration')
    assert hasattr(existing_tests, 'TestEndToEndStreaming')

    # Run a sample of existing tests to ensure compatibility
    detector = IncrementalToolDetector("qwen3")

    # Test from existing suite: basic initialization
    assert detector.model_name == "qwen3"
    assert detector.state == ToolDetectionState.SCANNING

    # Test from existing suite: basic tool detection
    content = '<|tool_call|>{"name": "test", "arguments": {}}</|tool_call|>'
    streamable, tools = detector.process_chunk(content)
    assert len(tools) == 1
    assert tools[0].name == "test"


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