"""Integration tests for the TTS Plugin Architecture.

These tests verify:
1. Consumer Flow: consumer → plugin → engine → session → synthesis → result
2. Lifecycle: dispose, no leaks, error handling
3. Regression: old path vs new path equivalence

Tests use mock plugins to avoid requiring real TTS dependencies.
"""

import pytest
from typing import Any, Iterator
from unittest.mock import MagicMock, patch

import numpy as np

from abogen.tts_plugin.engine import Engine, EngineSession
from abogen.tts_plugin.errors import EngineError
from abogen.tts_plugin.plugin_manager import PluginManager, get_plugin_manager, reset_plugin_manager
from abogen.tts_plugin.utils import Pipeline, create_pipeline
from abogen.tts_plugin.types import (
    AudioFormat,
    Duration,
    ParameterValues,
    SynthesisRequest,
    SynthesizedAudio,
    VoiceSelection,
)


class MockEngineSession:
    """Mock EngineSession that records calls for verification."""
    
    def __init__(self):
        self._disposed = False
        self.synthesize_calls = []
    
    def synthesize(self, request: SynthesisRequest) -> SynthesizedAudio:
        if self._disposed:
            raise EngineError("Session disposed")
        
        self.synthesize_calls.append(request)
        
        # Return fake audio
        audio = np.ones(1000, dtype=np.float32) * 0.5
        return SynthesizedAudio(
            data=audio.tobytes(),
            format=AudioFormat(mime="audio/wav", extension="wav"),
            duration=Duration(seconds=1000 / 24000),
        )
    
    def dispose(self) -> None:
        self._disposed = True


class MockEngine:
    """Mock Engine that creates MockEngineSessions."""
    
    def __init__(self, **kwargs):
        self.kwargs = kwargs
        self._disposed = False
        self.sessions_created = []
    
    def createSession(self) -> MockEngineSession:
        if self._disposed:
            raise EngineError("Engine disposed")
        session = MockEngineSession()
        self.sessions_created.append(session)
        return session
    
    def dispose(self) -> None:
        self._disposed = True


def create_mock_plugin(create_engine_func=None):
    """Helper to create a mock plugin module."""
    if create_engine_func is None:
        create_engine_func = lambda **kwargs: MockEngine(**kwargs)
    
    from abogen.tts_plugin.manifest import PluginManifest, EngineManifest
    
    manifest = PluginManifest(
        id="mock_tts",
        name="Mock TTS",
        version="1.0.0",
        api_version="1.0",
        description="Mock TTS for testing",
        author="Test",
        capabilities=(),
        requires=None,
        engine=EngineManifest(
            voiceSources=(),
            parameters=(),
            audioFormats=(),
        ),
    )
    
    return {
        "PLUGIN_MANIFEST": manifest,
        "MODEL_REQUIREMENTS": [],
        "create_engine": create_mock_plugin_engine if create_engine_func is None else create_engine_func,
    }


def create_mock_plugin_engine(**kwargs):
    """Default mock plugin engine factory."""
    return MockEngine(**kwargs)


class TestConsumerFlow:
    """Consumer Flow Test: consumer → plugin → engine → session → synthesis → result"""
    
    def test_full_consumer_flow(self):
        """Verify complete flow from consumer to audio output."""
        manager = PluginManager()
        
        # Register mock plugin
        mock_plugin = create_mock_plugin()
        manager._plugins["mock_tts"] = mock_plugin
        manager._loaded = True
        
        # Step 1: Consumer gets plugin
        assert manager.has_plugin("mock_tts") is True
        
        # Step 2: Plugin creates engine
        engine = manager.create_engine("mock_tts")
        assert engine is not None
        assert isinstance(engine, MockEngine)
        
        # Step 3: Engine creates session
        session = engine.createSession()
        assert session is not None
        assert isinstance(session, MockEngineSession)
        
        # Step 4: Session synthesizes
        request = SynthesisRequest(
            text="Hello world",
            voice=VoiceSelection(source="builtin", key="default"),
            parameters=ParameterValues(values={"speed": 1.0}),
            format=AudioFormat(mime="audio/wav", extension="wav"),
        )
        result = session.synthesize(request)
        
        # Step 5: Result returned
        assert result is not None
        assert isinstance(result, SynthesizedAudio)
        assert len(result.data) > 0
        assert result.format.mime == "audio/wav"
        assert result.duration.seconds > 0
    
    def test_consumer_flow_via_pipeline(self):
        """Verify flow through Pipeline utility matches direct flow."""
        manager = PluginManager()
        
        # Register mock plugin
        mock_plugin = create_mock_plugin()
        manager._plugins["mock_tts"] = mock_plugin
        manager._loaded = True
        
        # Use Pipeline utility
        with patch("abogen.tts_plugin.utils.get_plugin_manager", return_value=manager):
            backend = create_pipeline("mock_tts")
            
            # Call like old TTSBackend
            segments = list(backend("Hello world", voice="default", speed=1.0))
            
            # Verify result
            assert len(segments) >= 1
            segment = segments[0]
            assert hasattr(segment, "graphemes")
            assert hasattr(segment, "audio")
            assert segment.graphemes == "Hello world"


class TestLifecycle:
    """Lifecycle Test: dispose, no leaks, error handling"""
    
    def test_session_dispose_is_idempotent(self):
        """dispose() can be called multiple times safely."""
        session = MockEngineSession()
        
        session.dispose()
        session.dispose()  # Should not raise
        assert session._disposed is True
    
    def test_session_synthesize_after_dispose_raises(self):
        """synthesize() after dispose() raises EngineError."""
        session = MockEngineSession()
        session.dispose()
        
        request = SynthesisRequest(
            text="test",
            voice=VoiceSelection(source="builtin", key="default"),
            parameters=ParameterValues(),
            format=AudioFormat(mime="audio/wav", extension="wav"),
        )
        
        with pytest.raises(EngineError):
            session.synthesize(request)
    
    def test_engine_dispose_is_idempotent(self):
        """Engine dispose() can be called multiple times safely."""
        engine = MockEngine()
        
        engine.dispose()
        engine.dispose()  # Should not raise
        assert engine._disposed is True
    
    def test_engine_create_session_after_dispose_raises(self):
        """createSession() after dispose() raises EngineError."""
        engine = MockEngine()
        engine.dispose()
        
        with pytest.raises(EngineError):
            engine.createSession()
    
    def test_full_lifecycle(self):
        """Test complete lifecycle: create → use → dispose."""
        engine = MockEngine()
        
        # Create and use session
        session = engine.createSession()
        request = SynthesisRequest(
            text="test",
            voice=VoiceSelection(source="builtin", key="default"),
            parameters=ParameterValues(),
            format=AudioFormat(mime="audio/wav", extension="wav"),
        )
        result = session.synthesize(request)
        assert len(result.data) > 0
        
        # Dispose session
        session.dispose()
        assert session._disposed is True
        
        # Dispose engine
        engine.dispose()
        assert engine._disposed is True
    
    def test_no_session_leak_on_engine_dispose(self):
        """Engine can be disposed even if sessions were created."""
        engine = MockEngine()
        
        # Create multiple sessions
        session1 = engine.createSession()
        session2 = engine.createSession()
        
        # Use sessions
        request = SynthesisRequest(
            text="test",
            voice=VoiceSelection(source="builtin", key="default"),
            parameters=ParameterValues(),
            format=AudioFormat(mime="audio/wav", extension="wav"),
        )
        session1.synthesize(request)
        session2.synthesize(request)
        
        # Dispose engine (sessions still exist but engine is disposed)
        engine.dispose()
        assert engine._disposed is True
        
        # Sessions can still be used (they hold reference to pipeline)
        result = session1.synthesize(request)
        assert len(result.data) > 0
    
    def test_error_handling_in_synthesis(self):
        """Error during synthesis is handled correctly."""
        class FailingSession:
            def synthesize(self, request):
                raise EngineError("Synthesis failed")
            
            def dispose(self):
                pass
        
        session = FailingSession()
        request = SynthesisRequest(
            text="test",
            voice=VoiceSelection(source="builtin", key="default"),
            parameters=ParameterValues(),
            format=AudioFormat(mime="audio/wav", extension="wav"),
        )
        
        with pytest.raises(EngineError, match="Synthesis failed"):
            session.synthesize(request)


class TestRegression:
    """Regression Test: old path vs new path equivalence"""
    
    def test_old_path_vs_new_path_same_result(self):
        """Both paths should produce equivalent results."""
        # Setup mock plugin
        manager = PluginManager()
        mock_plugin = create_mock_plugin()
        manager._plugins["mock_tts"] = mock_plugin
        manager._loaded = True
        
        # New path: Plugin Manager → Engine → Session → Synthesis
        with patch("abogen.tts_plugin.utils.get_plugin_manager", return_value=manager):
            new_backend = create_pipeline("mock_tts")
            new_segments = list(new_backend("Hello world", voice="default", speed=1.0))
        
        # Old path: Direct MockEngine (simulating old registry)
        old_engine = MockEngine()
        old_session = old_engine.createSession()
        request = SynthesisRequest(
            text="Hello world",
            voice=VoiceSelection(source="builtin", key="default"),
            parameters=ParameterValues(values={"speed": 1.0}),
            format=AudioFormat(mime="audio/wav", extension="wav"),
        )
        old_result = old_session.synthesize(request)
        
        # Compare results
        # New path returns segments, old path returns SynthesizedAudio
        # But both should have valid audio data
        assert len(new_segments) >= 1
        assert len(old_result.data) > 0
        
        # Both should have same format
        assert new_segments[0].audio.dtype == np.float32
    
    def test_pipeline_matches_old_interface(self):
        """Pipeline utility should match old TTSBackend interface."""
        manager = PluginManager()
        mock_plugin = create_mock_plugin()
        manager._plugins["mock_tts"] = mock_plugin
        manager._loaded = True
        
        with patch("abogen.tts_plugin.utils.get_plugin_manager", return_value=manager):
            backend = create_pipeline("mock_tts", lang_code="a", device="cpu")
            
            # Old interface: pipeline(text, voice=..., speed=..., split_pattern=...)
            segments = list(backend(
                "Hello world",
                voice="af_heart",
                speed=1.0,
                split_pattern=r"\n+"
            ))
            
            # Should return segments with graphemes and audio
            assert len(segments) >= 1
            segment = segments[0]
            assert segment.graphemes == "Hello world"
            assert isinstance(segment.audio, np.ndarray)
            assert segment.audio.dtype == np.float32
            assert len(segment.audio) > 0


class TestPluginManagerIntegration:
    """Integration tests for PluginManager."""
    
    def test_plugin_manager_singleton_pattern(self):
        """Global plugin manager follows singleton pattern."""
        reset_plugin_manager()
        
        manager1 = get_plugin_manager()
        manager2 = get_plugin_manager()
        
        assert manager1 is manager2
        
        reset_plugin_manager()
        
        manager3 = get_plugin_manager()
        assert manager1 is not manager3
    
    def test_plugin_manager_discover_plugins(self):
        """Plugin manager can discover plugins from directory."""
        manager = PluginManager()
        
        # Discover from test plugins directory
        manager.discover("tests/plugins")
        
        # Should find valid_plugin
        # (This depends on test plugins existing)
        plugins = manager.list_plugins()
        assert isinstance(plugins, list)
    
    def test_plugin_manager_dispose_all(self):
        """Plugin manager can dispose all cached engines."""
        manager = PluginManager()
        
        # Register mock plugin
        mock_plugin = create_mock_plugin()
        manager._plugins["mock_tts"] = mock_plugin
        manager._loaded = True
        
        # Create engines
        engine1 = manager.get_or_create_engine("mock_tts")
        engine2 = manager.get_or_create_engine("mock_tts")
        
        # Dispose all
        manager.dispose_all()
        
        # Engines should be disposed
        assert engine1._disposed is True
        assert engine2._disposed is True
        
        # Cache should be empty
        assert len(manager._engines) == 0


class TestNoCompatLayer:
    """Regression: confirm the compatibility layer has been removed."""

    def test_compat_module_does_not_exist(self):
        """abogen.tts_plugin.compat must not be importable."""
        import importlib
        with pytest.raises((ImportError, ModuleNotFoundError)):
            importlib.import_module("abogen.tts_plugin.compat")

    def test_consumers_use_plugin_architecture_directly(self):
        """Key consumers import from abogen.tts_plugin.utils, not compat."""
        import inspect, abogen.voice_profiles, abogen.voice_formulas, abogen.voice_cache

        for mod in (abogen.voice_profiles, abogen.voice_formulas, abogen.voice_cache):
            source = inspect.getsource(mod)
            assert "tts_plugin.compat" not in source, (
                f"{mod.__name__} still references tts_plugin.compat"
            )
