"""Unit coverage for the declarative built-in backend metadata tables (M6b).

The five roles with built-in tables
(transport/vad/noise_reducer/echo_canceller/agent) resolve their bundled
backends here; registered audio-stage extensions supplement them through
provider catalogs. Every backend must map to the right config-type / extra /
required_env / probe-module / capabilities.
"""

from __future__ import annotations

import pytest

from easycat.planning.transport_registry import (
    AGENT_BACKENDS,
    BUILTIN_BACKEND_ROLES,
    ECHO_CANCELLER_BACKENDS,
    EXTRA_PROBE_MODULE,
    NOISE_REDUCER_BACKENDS,
    NON_CATALOG_ROLES,
    TRANSPORT_BACKENDS,
    VAD_BACKENDS,
    probe_module_for_extra,
)


def test_transport_backends_cover_manifest_shortcuts() -> None:
    from easycat.project.schema import TRANSPORT_SHORTCUTS

    assert set(TRANSPORT_BACKENDS) == set(TRANSPORT_SHORTCUTS)
    assert TRANSPORT_BACKENDS["webrtc"].extra == "webrtc"
    assert TRANSPORT_BACKENDS["websocket"].extra is None  # stdlib websockets
    assert TRANSPORT_BACKENDS["twilio"].extra == "telephony"
    assert TRANSPORT_BACKENDS["local"].extra == "local"
    assert TRANSPORT_BACKENDS["webrtc"].config_type == "WebRTCTransportConfig"


def test_vad_backends_cover_vad_literal() -> None:
    from easycat.vad._base import _VALID_VAD_BACKENDS

    assert set(VAD_BACKENDS) == set(_VALID_VAD_BACKENDS)
    assert VAD_BACKENDS["silero"].extra == "silero-vad"
    assert VAD_BACKENDS["ten"].extra == "ten-vad"
    assert VAD_BACKENDS["funasr"].extra == "funasr-vad"
    # Krisp is a commercial SDK with no pyproject extra.
    assert VAD_BACKENDS["krisp"].extra is None


def test_noise_reducer_backends_cover_backend_literal() -> None:
    from easycat.noise_reduction import _VALID_NOISE_REDUCER_BACKENDS

    assert set(NOISE_REDUCER_BACKENDS) == set(_VALID_NOISE_REDUCER_BACKENDS)
    assert NOISE_REDUCER_BACKENDS["rnnoise"].extra == "rnnoise"
    assert NOISE_REDUCER_BACKENDS["krisp"].extra is None


def test_echo_canceller_backends() -> None:
    assert ECHO_CANCELLER_BACKENDS["livekit"].extra == "aec"
    assert ECHO_CANCELLER_BACKENDS["passthrough"].extra is None
    assert "echo_cancellation" in ECHO_CANCELLER_BACKENDS["livekit"].capabilities


def test_agent_backend_has_no_default_env_or_extra() -> None:
    python_backend = AGENT_BACKENDS["python"]
    assert python_backend.extra is None
    assert python_backend.required_env is None
    assert "custom_agent" in python_backend.capabilities


def test_capabilities_are_declared_frozensets() -> None:
    for table in (
        TRANSPORT_BACKENDS,
        VAD_BACKENDS,
        NOISE_REDUCER_BACKENDS,
        ECHO_CANCELLER_BACKENDS,
        AGENT_BACKENDS,
    ):
        for backend in table.values():
            assert isinstance(backend.capabilities, frozenset)


def test_extra_probe_module_covers_every_named_extra() -> None:
    # Every extra named by any backend table must have a probe-module entry so
    # find_spec works uniformly.
    named_extras: set[str] = set()
    for table in (
        TRANSPORT_BACKENDS,
        VAD_BACKENDS,
        NOISE_REDUCER_BACKENDS,
        ECHO_CANCELLER_BACKENDS,
        AGENT_BACKENDS,
    ):
        named_extras.update(b.extra for b in table.values() if b.extra is not None)
    # Plus the STT/TTS catalog extras.
    from easycat._provider_registry import provider_extras

    named_extras.update(e for e in provider_extras().values() if e)

    missing = named_extras - set(EXTRA_PROBE_MODULE)
    assert not missing, f"EXTRA_PROBE_MODULE missing entries for: {sorted(missing)}"


def test_probe_module_for_extra_resolves() -> None:
    assert probe_module_for_extra(None) is None
    # Providers backed entirely by core HTTP/WebSocket dependencies map to
    # None (nothing additional to probe).
    assert probe_module_for_extra("openai") is None
    assert probe_module_for_extra("deepgram") is None
    assert probe_module_for_extra("silero-vad") == "onnxruntime"
    assert probe_module_for_extra("webrtc") == "aiortc"


def test_probe_module_for_unmapped_third_party_extra_falls_back_to_extra_name() -> None:
    # A registered third-party provider may carry an arbitrary extra with no
    # built-in mapping. Rather than crash the planner (a KeyError pinned
    # /health/ready), probe the extra NAME itself so a genuinely-missing install
    # is still flagged. (Built-in completeness is guarded separately above.)
    assert probe_module_for_extra("acme-stt") == "acme-stt"


def test_transport_aec_defaults_match_manifest_resolved_easyconfig(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    # The planner's per-transport ``default_echo_cancellation_enabled`` mirror is
    # consumed ONLY by the manifest path (``_select_from_profile``), so tie it to
    # what ``to_easyconfig`` ACTUALLY resolves per transport — NOT the bare
    # transport-config ClassVar, which the browser/phone presets override (webrtc's
    # ClassVar is False but ``EasyConfig.browser`` forces AEC on). This catches a
    # silent drift if a preset OR the mirror changes.
    from easycat.project.manifest import ProjectManifest
    from easycat.project.schema import (
        ProjectSection,
        ServerSection,
        VoiceProfile,
        parse_auth_reference,
    )

    monkeypatch.setenv("OPENAI_API_KEY", "sk-registry-test")
    monkeypatch.setenv("TWILIO_STREAM_TOKEN_SECRET", "twilio-registry-test")

    for shortcut, backend in TRANSPORT_BACKENDS.items():
        token = (
            parse_auth_reference(
                "bearer-env:TWILIO_STREAM_TOKEN_SECRET",
                field_name="voice.default.token",
            )
            if shortcut == "twilio"
            else None
        )
        profile = VoiceProfile(name="default", transport=shortcut, token=token)
        manifest = ProjectManifest(
            project=ProjectSection(), server=ServerSection(), profiles={"default": profile}
        )
        config = manifest.to_easyconfig("default", resolve_agent=False)
        resolved = bool(config.echo_cancellation and config.echo_cancellation.enabled)
        assert resolved == backend.default_echo_cancellation_enabled, shortcut


def test_builtin_backend_roles_are_the_five() -> None:
    expected = {
        "transport",
        "vad",
        "agent",
        "noise_reducer",
        "echo_canceller",
    }
    assert set(BUILTIN_BACKEND_ROLES) == expected
    assert NON_CATALOG_ROLES == BUILTIN_BACKEND_ROLES
