from __future__ import annotations

from collections.abc import AsyncIterator

import pytest

from easycat.audio_format import AudioChunk
from easycat.events import Event, STTEvent, TTSEvent
from easycat.runtime.capabilities import (
    aclose_if_supported,
    bind_identity_sink_if_supported,
    clear_audio_if_supported,
    health_checkable,
    playback_acknowledgements,
    rollback_warmup_if_supported,
    transport_reports_audio_delivery,
    warmup_if_supported,
    warmupable,
)
from easycat.session._session import Session
from easycat.session._types import SessionConfig
from easycat.tts.input import TTSInput


class _ActiveSTT:
    async def start_stream(self) -> None:
        pass

    async def send_audio(self, chunk: AudioChunk) -> None:
        pass

    async def commit_segment(self) -> bool:
        return False

    async def end_stream(self) -> None:
        pass

    async def events(self) -> AsyncIterator[STTEvent]:
        return
        yield


class _ActiveTTS:
    async def synthesize(self, payload: TTSInput | str) -> AsyncIterator[TTSEvent]:
        return
        yield

    async def stop(self) -> None:
        pass

    async def cancel(self) -> None:
        pass


class _ActiveVAD:
    async def process(self, chunk: AudioChunk) -> AsyncIterator[Event]:
        return
        yield

    def configure(self, **kwargs: object) -> None:
        pass


class _ActiveTransport:
    async def connect(self) -> None:
        pass

    async def disconnect(self) -> None:
        pass

    async def receive_audio(self) -> AsyncIterator[AudioChunk]:
        return
        yield

    async def send_audio(self, chunk: AudioChunk) -> bool:
        return True

    async def clear_audio(self) -> None:
        pass


class _ActiveAgent:
    async def run(self, text: str) -> str:
        return text


class _PassthroughSTT(_ActiveSTT):
    is_passthrough_provider = True


class _PassthroughTTS(_ActiveTTS):
    is_passthrough_provider = True


class _PassthroughVAD(_ActiveVAD):
    is_passthrough_provider = True


class _PassthroughTransport(_ActiveTransport):
    is_passthrough_provider = True


class _ActiveNoiseReducer:
    async def process(self, chunk: AudioChunk) -> AudioChunk:
        return chunk


class _PassthroughNoiseReducer(_ActiveNoiseReducer):
    is_passthrough_provider = True


class _ActiveEchoCanceller:
    async def process(self, chunk: AudioChunk) -> AudioChunk:
        return chunk

    def feed_reference(self, chunk: AudioChunk) -> None:
        pass


class _PassthroughEchoCanceller(_ActiveEchoCanceller):
    is_passthrough_provider = True


class _CapabilityTransport(_ActiveTransport):
    reports_audio_delivery = True

    async def send_playback_mark(self, name: str | None = None) -> str:
        return name or "custom_mark"


class _DelegatedCapabilities(_CapabilityTransport):
    def __init__(self) -> None:
        self.cleared = False
        self.closed = False
        self.identity_sink = None

    async def clear_audio(self) -> None:
        self.cleared = True

    def bind_identity_sink(self, sink: object) -> None:
        self.identity_sink = sink

    async def health_check(self) -> bool:
        return True

    async def aclose(self) -> None:
        self.closed = True


class _CapabilityProxy:
    def __init__(self, target: object) -> None:
        self._target = target

    def __getattr__(self, name: str) -> object:
        return getattr(self._target, name)


class _AsyncWarmupProvider:
    def __init__(self) -> None:
        self.warmed = False

    async def warmup(self) -> None:
        self.warmed = True


class _SyncWarmupProvider:
    def __init__(self) -> None:
        self.warmed = False

    def warmup(self) -> None:
        self.warmed = True


def _config(**overrides: object) -> SessionConfig:
    values = {
        "stt": _ActiveSTT(),
        "tts": _ActiveTTS(),
        "vad": _ActiveVAD(),
        "transport": _ActiveTransport(),
        "agent": _ActiveAgent(),
    }
    values.update(overrides)
    return SessionConfig(**values)


@pytest.mark.parametrize(
    ("field", "provider", "expected_name"),
    [
        ("stt", _PassthroughSTT(), "stt"),
        ("tts", _PassthroughTTS(), "tts"),
        ("vad", _PassthroughVAD(), "vad"),
        ("transport", _PassthroughTransport(), "transport"),
    ],
)
def test_session_validation_rejects_custom_passthrough_provider_markers(
    field: str,
    provider: object,
    expected_name: str,
) -> None:
    with pytest.raises(ValueError, match=expected_name):
        Session(_config(**{field: provider}))


def test_session_validation_warns_on_custom_passthrough_noise_when_enabled(
    caplog: pytest.LogCaptureFixture,
) -> None:
    # A passthrough noise reducer is legitimate graceful degradation (mirrors
    # PassthroughAEC), so enabling noise reduction without a real backend must
    # warn-and-continue rather than crash at construction. fail-loud is opt-in
    # via NoiseReducerConfig(fallback_policy="error").
    with caplog.at_level("WARNING", logger="easycat.session._session"):
        session = Session(
            _config(
                noise_reducer=_PassthroughNoiseReducer(),
                enable_noise_reduction=True,
            )
        )

    assert session._enable_noise_reduction is True
    assert any(
        "passthrough" in record.message and "Noise reduction is enabled" in record.message
        for record in caplog.records
    )
    assert any("uv add 'easycat[rnnoise]'" in record.message for record in caplog.records)
    assert any(
        "uv sync --extra rnnoise --group dev" in record.message for record in caplog.records
    )


def test_custom_passthrough_processors_do_not_auto_enable_features() -> None:
    session = Session(
        _config(
            noise_reducer=_PassthroughNoiseReducer(),
            echo_canceller=_PassthroughEchoCanceller(),
        )
    )

    assert session._enable_noise_reduction is False
    assert session._enable_aec is False


def test_custom_active_processors_still_auto_enable_features() -> None:
    session = Session(
        _config(
            noise_reducer=_ActiveNoiseReducer(),
            echo_canceller=_ActiveEchoCanceller(),
        )
    )

    assert session._enable_noise_reduction is True
    assert session._enable_aec is True


def test_transport_capabilities_are_detected_structurally() -> None:
    transport = _CapabilityTransport()
    session = Session(_config(transport=transport))

    assert session._audio_router._playback_ack_transport is transport
    assert session._audio_router._transport_reports_audio_delivery is True


@pytest.mark.asyncio
async def test_capability_helpers_support_getattr_delegation() -> None:
    target = _DelegatedCapabilities()
    proxy = _CapabilityProxy(target)
    sink = object()

    playback = playback_acknowledgements(proxy)
    assert playback is not None
    assert await playback.send_playback_mark() == "custom_mark"

    await clear_audio_if_supported(proxy)
    assert target.cleared is True

    assert bind_identity_sink_if_supported(proxy, sink) is True
    assert target.identity_sink is sink

    health = health_checkable(proxy)
    assert health is not None
    assert await health.health_check() is True

    await aclose_if_supported(proxy)
    assert target.closed is True
    assert transport_reports_audio_delivery(proxy) is True


@pytest.mark.asyncio
async def test_warmup_capability_accepts_sync_and_async_hooks() -> None:
    async_provider = _AsyncWarmupProvider()
    sync_provider = _SyncWarmupProvider()

    assert warmupable(async_provider) is async_provider
    assert warmupable(sync_provider) is sync_provider
    assert await warmup_if_supported(async_provider) is True
    assert await warmup_if_supported(sync_provider) is True
    assert await warmup_if_supported(object()) is False
    assert async_provider.warmed is True
    assert sync_provider.warmed is True


@pytest.mark.asyncio
async def test_warmup_rollback_capability_accepts_sync_and_async_hooks() -> None:
    class AsyncRollback:
        def __init__(self) -> None:
            self.rolled_back = False

        async def rollback_warmup(self) -> None:
            self.rolled_back = True

    class SyncRollback:
        def __init__(self) -> None:
            self.rolled_back = False

        def rollback_warmup(self) -> None:
            self.rolled_back = True

    async_provider = AsyncRollback()
    sync_provider = SyncRollback()

    await rollback_warmup_if_supported(async_provider)
    await rollback_warmup_if_supported(sync_provider)
    await rollback_warmup_if_supported(object())

    assert async_provider.rolled_back is True
    assert sync_provider.rolled_back is True
