"""
Unit tests for MultiModalAdapter - provider abstraction for multi-modal processing.

Following TDD methodology: Write tests FIRST, then implement.
"""

import pytest
from pathlib import Path
from typing import Dict, Any, Optional
from unittest.mock import Mock, patch, MagicMock

# Test infrastructure
try:
    from kaizen.providers.multi_modal_adapter import (
        MultiModalAdapter,
        OllamaMultiModalAdapter,
        OpenAIMultiModalAdapter,
        get_multi_modal_adapter
    )
    from kaizen.signatures.multi_modal import ImageField, AudioField
    IMPORTS_AVAILABLE = True
except ImportError:
    IMPORTS_AVAILABLE = False

pytestmark = pytest.mark.skipif(not IMPORTS_AVAILABLE, reason="MultiModalAdapter not yet implemented")


class TestMultiModalAdapterInterface:
    """Test MultiModalAdapter abstract interface."""

    def test_adapter_interface_definition(self):
        """Test that MultiModalAdapter defines required interface."""
        # Should be abstract base class
        from abc import ABC
        assert issubclass(MultiModalAdapter, ABC)

        # Should define required methods
        required_methods = [
            'is_available',
            'supports_vision',
            'supports_audio',
            'process_multi_modal',
            'estimate_cost'
        ]

        for method in required_methods:
            assert hasattr(MultiModalAdapter, method)

    def test_adapter_cannot_instantiate_directly(self):
        """Test that MultiModalAdapter cannot be instantiated directly."""
        with pytest.raises(TypeError):
            adapter = MultiModalAdapter()


class TestOllamaMultiModalAdapter:
    """Test Ollama multi-modal adapter implementation."""

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_adapter_creation(self):
        """Test basic adapter creation."""
        adapter = OllamaMultiModalAdapter(model="llava:13b")
        assert adapter is not None
        assert adapter.model == "llava:13b"

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_availability_check(self):
        """Test adapter availability check."""
        adapter = OllamaMultiModalAdapter()
        assert adapter.is_available() is True

    @patch('kaizen.providers.OLLAMA_AVAILABLE', False)
    def test_unavailable_when_ollama_missing(self):
        """Test adapter reports unavailable when Ollama not installed."""
        adapter = OllamaMultiModalAdapter()
        assert adapter.is_available() is False

    def test_supports_vision(self):
        """Test that Ollama adapter supports vision."""
        with patch('kaizen.providers.OLLAMA_AVAILABLE', True):
            adapter = OllamaMultiModalAdapter(model="llava:13b")
            assert adapter.supports_vision() is True

    def test_supports_audio_via_whisper(self):
        """Test that Ollama adapter supports audio via Whisper integration."""
        with patch('kaizen.providers.OLLAMA_AVAILABLE', True):
            adapter = OllamaMultiModalAdapter()
            # Audio supported via WhisperProcessor
            assert adapter.supports_audio() is True

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_process_vision_only(self, tmp_path):
        """Test processing with vision only."""
        # Create test image
        from PIL import Image
        image_path = tmp_path / "test.png"
        Image.new('RGB', (100, 100)).save(image_path)

        adapter = OllamaMultiModalAdapter(model="llava:13b")

        with patch.object(adapter, '_call_ollama_vision') as mock_vision:
            mock_vision.return_value = {'description': 'Test image'}

            result = adapter.process_multi_modal(
                image=str(image_path),
                prompt="Describe this image"
            )

            assert 'description' in result
            mock_vision.assert_called_once()

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_process_audio_only(self, tmp_path):
        """Test processing with audio only."""
        # Create test audio
        import wave
        import struct
        audio_path = tmp_path / "test.wav"
        with wave.open(str(audio_path), 'wb') as wav:
            wav.setnchannels(1)
            wav.setsampwidth(2)
            wav.setframerate(16000)
            for _ in range(16000):
                wav.writeframes(struct.pack('<h', 0))

        adapter = OllamaMultiModalAdapter()

        with patch.object(adapter, '_call_whisper') as mock_whisper:
            mock_whisper.return_value = {'text': 'Test transcription'}

            result = adapter.process_multi_modal(
                audio=str(audio_path)
            )

            assert 'text' in result
            mock_whisper.assert_called_once()

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_process_mixed_modalities(self, tmp_path):
        """Test processing with image + audio + text."""
        # Create test files
        from PIL import Image
        image_path = tmp_path / "test.png"
        Image.new('RGB', (100, 100)).save(image_path)

        import wave
        import struct
        audio_path = tmp_path / "test.wav"
        with wave.open(str(audio_path), 'wb') as wav:
            wav.setnchannels(1)
            wav.setsampwidth(2)
            wav.setframerate(16000)
            for _ in range(16000):
                wav.writeframes(struct.pack('<h', 0))

        adapter = OllamaMultiModalAdapter(model="llava:13b")

        with patch.object(adapter, '_call_ollama_vision') as mock_vision, \
             patch.object(adapter, '_call_whisper') as mock_whisper, \
             patch.object(adapter, '_combine_results') as mock_combine:

            mock_vision.return_value = {'visual': 'Image analysis'}
            mock_whisper.return_value = {'audio': 'Audio transcription'}
            mock_combine.return_value = {'combined': 'Full analysis'}

            result = adapter.process_multi_modal(
                image=str(image_path),
                audio=str(audio_path),
                text="Analyze this media"
            )

            assert 'combined' in result
            mock_vision.assert_called_once()
            mock_whisper.assert_called_once()
            mock_combine.assert_called_once()

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_estimate_cost_always_zero(self):
        """Test that Ollama adapter always estimates $0 cost."""
        adapter = OllamaMultiModalAdapter()

        # Vision cost
        vision_cost = adapter.estimate_cost(modality='vision', input_size=1000)
        assert vision_cost == 0.0

        # Audio cost
        audio_cost = adapter.estimate_cost(modality='audio', duration=60)
        assert audio_cost == 0.0

        # Mixed cost
        mixed_cost = adapter.estimate_cost(modality='mixed')
        assert mixed_cost == 0.0

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_model_auto_download(self):
        """Test automatic model download when missing."""
        with patch('kaizen.providers.ollama_model_manager.OllamaModelManager') as MockManager:
            mock_manager = MockManager.return_value
            mock_manager.ensure_model_available.return_value = True

            adapter = OllamaMultiModalAdapter(
                model="llava:13b",
                auto_download=True
            )

            adapter._ensure_model_ready()

            mock_manager.ensure_model_available.assert_called_with(
                "llava:13b",
                auto_download=True
            )


class TestOpenAIMultiModalAdapter:
    """Test OpenAI multi-modal adapter implementation."""

    def test_adapter_creation(self):
        """Test basic adapter creation."""
        adapter = OpenAIMultiModalAdapter(api_key="test-key")
        assert adapter is not None
        assert adapter.api_key == "test-key"

    def test_availability_check_with_key(self):
        """Test availability when API key is present."""
        adapter = OpenAIMultiModalAdapter(api_key="test-key")
        assert adapter.is_available() is True

    def test_unavailable_without_key(self):
        """Test unavailable when no API key."""
        adapter = OpenAIMultiModalAdapter(api_key=None)
        assert adapter.is_available() is False

    def test_supports_vision(self):
        """Test that OpenAI adapter supports vision (GPT-4V)."""
        adapter = OpenAIMultiModalAdapter(api_key="test-key")
        assert adapter.supports_vision() is True

    def test_supports_audio(self):
        """Test that OpenAI adapter supports audio (Whisper)."""
        adapter = OpenAIMultiModalAdapter(api_key="test-key")
        assert adapter.supports_audio() is True

    def test_process_vision_only(self, tmp_path):
        """Test processing with vision only."""
        from PIL import Image
        image_path = tmp_path / "test.png"
        Image.new('RGB', (100, 100)).save(image_path)

        adapter = OpenAIMultiModalAdapter(api_key="test-key")

        with patch.object(adapter, '_call_openai_vision') as mock_vision:
            mock_vision.return_value = {'description': 'Test image via OpenAI'}

            result = adapter.process_multi_modal(
                image=str(image_path),
                prompt="Describe this image"
            )

            assert 'description' in result
            mock_vision.assert_called_once()

    def test_process_audio_only(self, tmp_path):
        """Test processing with audio only."""
        import wave
        import struct
        audio_path = tmp_path / "test.wav"
        with wave.open(str(audio_path), 'wb') as wav:
            wav.setnchannels(1)
            wav.setsampwidth(2)
            wav.setframerate(16000)
            for _ in range(16000):
                wav.writeframes(struct.pack('<h', 0))

        adapter = OpenAIMultiModalAdapter(api_key="test-key")

        with patch.object(adapter, '_call_openai_whisper') as mock_whisper:
            mock_whisper.return_value = {'text': 'OpenAI transcription'}

            result = adapter.process_multi_modal(
                audio=str(audio_path)
            )

            assert 'text' in result
            mock_whisper.assert_called_once()

    def test_estimate_cost_vision(self):
        """Test cost estimation for vision processing."""
        adapter = OpenAIMultiModalAdapter(api_key="test-key")

        # GPT-4V costs ~$0.01 per image
        cost = adapter.estimate_cost(modality='vision', input_size=1000)
        assert cost > 0
        assert 0.005 <= cost <= 0.02  # Reasonable range

    def test_estimate_cost_audio(self):
        """Test cost estimation for audio processing."""
        adapter = OpenAIMultiModalAdapter(api_key="test-key")

        # Whisper costs $0.006 per minute
        cost = adapter.estimate_cost(modality='audio', duration=60)
        assert cost > 0
        assert 0.005 <= cost <= 0.01  # ~$0.006

    def test_cost_warning_enabled(self):
        """Test cost warning before OpenAI calls."""
        adapter = OpenAIMultiModalAdapter(
            api_key="test-key",
            warn_before_call=True
        )

        assert adapter.warn_before_call is True

    def test_usage_tracking(self, tmp_path):
        """Test that adapter tracks API usage."""
        from PIL import Image
        image_path = tmp_path / "test.png"
        Image.new('RGB', (100, 100)).save(image_path)

        adapter = OpenAIMultiModalAdapter(api_key="test-key")

        # Mock OpenAI client instead of internal method so usage tracking still works
        with patch('openai.OpenAI') as MockClient:
            mock_response = Mock()
            mock_response.choices = [Mock(message=Mock(content='Test description'))]
            MockClient.return_value.chat.completions.create.return_value = mock_response

            adapter.process_multi_modal(image=str(image_path), prompt="Test")

            # Should track usage
            usage = adapter.get_usage_stats()
            assert usage['total_calls'] == 1
            assert usage['vision_calls'] == 1
            assert usage['total_cost'] == pytest.approx(0.01, abs=0.001)


class TestAdapterFactory:
    """Test adapter factory and auto-selection."""

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_get_ollama_adapter_when_available(self):
        """Test automatic selection of Ollama when available."""
        adapter = get_multi_modal_adapter(prefer_local=True)
        assert isinstance(adapter, OllamaMultiModalAdapter)

    @patch('kaizen.providers.OLLAMA_AVAILABLE', False)
    def test_get_openai_adapter_when_ollama_unavailable(self):
        """Test fallback to OpenAI when Ollama unavailable."""
        # Clear cache first
        from kaizen.providers.multi_modal_adapter import _adapter_cache
        _adapter_cache.clear()

        with patch.dict('os.environ', {'OPENAI_API_KEY': 'test-key'}):
            adapter = get_multi_modal_adapter(prefer_local=True)
            assert isinstance(adapter, OpenAIMultiModalAdapter)

    def test_explicit_provider_selection(self):
        """Test explicit provider selection."""
        # Clear cache
        from kaizen.providers.multi_modal_adapter import _adapter_cache
        _adapter_cache.clear()

        # Explicitly request OpenAI
        adapter = get_multi_modal_adapter(
            provider='openai',
            api_key='test-key'
        )
        assert isinstance(adapter, OpenAIMultiModalAdapter)

        # Clear cache again
        _adapter_cache.clear()

        # Explicitly request Ollama
        with patch('kaizen.providers.OLLAMA_AVAILABLE', True):
            adapter = get_multi_modal_adapter(provider='ollama')
            assert isinstance(adapter, OllamaMultiModalAdapter)

    def test_error_when_no_adapter_available(self):
        """Test error when no adapter is available."""
        # Clear cache first
        from kaizen.providers.multi_modal_adapter import _adapter_cache
        _adapter_cache.clear()

        with patch('kaizen.providers.OLLAMA_AVAILABLE', False), \
             patch.dict('os.environ', {}, clear=True):
            with pytest.raises(ValueError, match="No multi-modal adapter available"):
                get_multi_modal_adapter()

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_adapter_caching(self):
        """Test that adapters are cached for reuse."""
        adapter1 = get_multi_modal_adapter(prefer_local=True)
        adapter2 = get_multi_modal_adapter(prefer_local=True)

        # Should return same instance (cached)
        assert adapter1 is adapter2


class TestAdapterIntegration:
    """Test adapter integration with signature system."""

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_image_field_integration(self, tmp_path):
        """Test ImageField integration with adapter."""
        from PIL import Image
        image_path = tmp_path / "test.png"
        Image.new('RGB', (100, 100)).save(image_path)

        adapter = OllamaMultiModalAdapter(model="llava:13b")

        # ImageField should be preprocessed by adapter
        image_field = ImageField()
        image_field.load(str(image_path))

        with patch.object(adapter, '_call_ollama_vision') as mock_vision:
            mock_vision.return_value = {'result': 'Success'}

            result = adapter.process_multi_modal(image=image_field)

            assert 'result' in result

    @patch('kaizen.providers.OLLAMA_AVAILABLE', True)
    def test_audio_field_integration(self, tmp_path):
        """Test AudioField integration with adapter."""
        import wave
        import struct
        audio_path = tmp_path / "test.wav"
        with wave.open(str(audio_path), 'wb') as wav:
            wav.setnchannels(1)
            wav.setsampwidth(2)
            wav.setframerate(16000)
            for _ in range(16000):
                wav.writeframes(struct.pack('<h', 0))

        adapter = OllamaMultiModalAdapter()

        # AudioField should be preprocessed by adapter
        audio_field = AudioField()
        audio_field.load(str(audio_path))

        with patch.object(adapter, '_call_whisper') as mock_whisper:
            mock_whisper.return_value = {'text': 'Success'}

            result = adapter.process_multi_modal(audio=audio_field)

            assert 'text' in result


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