"""Unit tests for adapters/google-adk/adapter.py.

Coverage targets (100%)
-----------------------
- Module constants: _DEFAULT_AGENT_NAME, _DEFAULT_MAX_OUTPUT_TOKENS, etc.
- MissingContent sentinel class
- GoogleADKA2AExecutor.__init__    — field assignment + runner injection
- GoogleADKA2AExecutor._extract_text
- GoogleADKA2AExecutor._build_content
- GoogleADKA2AExecutor._ensure_session — first call (create), subsequent call (skip)
- GoogleADKA2AExecutor.execute     — happy path, empty input, API error,
                                     no final_response events, partial text
- GoogleADKA2AExecutor.cancel      — TaskStatusUpdateEvent emitted
- GoogleADKAdapter.name / display_name / description / get_config_schema
- GoogleADKAdapter.setup           — success, missing key, vertex override
- GoogleADKAdapter.create_executor — model stripping, defaults, rc overrides
- Adapter alias

All google-adk, google-genai, and shared_runtime calls are mocked.
No live API calls are made.
"""
from __future__ import annotations

import sys
from types import ModuleType
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Stub heavy external modules BEFORE the adapter is imported.
# conftest.py already stubs: a2a, builtin_tools, langchain_core.
# We need to additionally stub: google.adk, google.genai, shared_runtime.
# ---------------------------------------------------------------------------


def _make_a2a_stubs() -> None:
    """Register minimal a2a SDK stubs in sys.modules.

    Mirrors what workspace/tests/conftest.py does; needed because
    this test file lives outside the ``tests/`` directory and conftest.py
    is not automatically loaded for it.
    """
    if "a2a" in sys.modules:
        # Already mocked by conftest — just ensure new_agent_text_message is passthrough
        a2a_utils = sys.modules.get("a2a.utils")
        if a2a_utils and callable(getattr(a2a_utils, "new_agent_text_message", None)):
            a2a_utils.new_agent_text_message = lambda text, **kwargs: text
        return

    agent_execution_mod = ModuleType("a2a.server.agent_execution")

    class AgentExecutor:
        pass

    class RequestContext:
        pass

    agent_execution_mod.AgentExecutor = AgentExecutor
    agent_execution_mod.RequestContext = RequestContext

    events_mod = ModuleType("a2a.server.events")

    class EventQueue:
        pass

    events_mod.EventQueue = EventQueue

    tasks_mod = ModuleType("a2a.server.tasks")
    types_mod = ModuleType("a2a.types")

    class Part:
        # v1: Part takes text= directly; root= retained for compat during transition
        def __init__(self, text=None, root=None, **kwargs):
            self.text = text

    types_mod.Part = Part

    # a2a.helpers (v1: moved from a2a.utils)
    helpers_mod = ModuleType("a2a.helpers")
    # Passthrough so tests can assert on the plain text string, matching the
    # hermes_executor test convention from conftest.py.
    helpers_mod.new_agent_text_message = lambda text, **kwargs: text

    a2a_mod = ModuleType("a2a")
    a2a_server_mod = ModuleType("a2a.server")

    sys.modules["a2a"] = a2a_mod
    sys.modules["a2a.server"] = a2a_server_mod
    sys.modules["a2a.server.agent_execution"] = agent_execution_mod
    sys.modules["a2a.server.events"] = events_mod
    sys.modules["a2a.server.tasks"] = tasks_mod
    sys.modules["a2a.types"] = types_mod
    sys.modules["a2a.helpers"] = helpers_mod


def _make_google_adk_stubs() -> None:
    """Register minimal google.adk and google.genai stubs in sys.modules."""
    # google (top-level namespace package)
    google_mod = sys.modules.get("google") or ModuleType("google")
    google_mod.__path__ = []
    sys.modules.setdefault("google", google_mod)

    # google.genai
    google_genai_mod = ModuleType("google.genai")
    google_genai_mod.__path__ = []

    google_genai_types_mod = ModuleType("google.genai.types")

    class _Content:
        def __init__(self, role="user", parts=None):
            self.role = role
            self.parts = parts or []

    class _Part:
        def __init__(self, text=""):
            self.text = text

    google_genai_types_mod.Content = _Content
    google_genai_types_mod.Part = _Part

    sys.modules["google.genai"] = google_genai_mod
    sys.modules["google.genai.types"] = google_genai_types_mod

    # google.adk
    google_adk_mod = ModuleType("google.adk")
    google_adk_mod.__path__ = []

    # google.adk.agents
    google_adk_agents_mod = ModuleType("google.adk.agents")

    class _LlmAgent:
        def __init__(self, name="", model="", instruction="", tools=None):
            self.name = name
            self.model = model
            self.instruction = instruction
            self.tools = tools or []

    google_adk_agents_mod.LlmAgent = _LlmAgent

    # google.adk.runners
    google_adk_runners_mod = ModuleType("google.adk.runners")

    class _Runner:
        def __init__(self, agent=None, app_name="", session_service=None):
            self.agent = agent
            self.app_name = app_name
            self.session_service = session_service

        async def run_async(self, session_id, user_id, new_message):
            # Stub — tests override this via mock runner
            return
            yield  # make it an async generator

    google_adk_runners_mod.Runner = _Runner

    # google.adk.sessions
    google_adk_sessions_mod = ModuleType("google.adk.sessions")

    class _InMemorySessionService:
        def __init__(self):
            self._sessions: dict = {}

        async def get_session(self, app_name, user_id, session_id):
            return self._sessions.get((app_name, user_id, session_id))

        async def create_session(self, app_name, user_id, session_id):
            self._sessions[(app_name, user_id, session_id)] = {"id": session_id}
            return self._sessions[(app_name, user_id, session_id)]

    google_adk_sessions_mod.InMemorySessionService = _InMemorySessionService

    sys.modules["google.adk"] = google_adk_mod
    sys.modules["google.adk.agents"] = google_adk_agents_mod
    sys.modules["google.adk.runners"] = google_adk_runners_mod
    sys.modules["google.adk.sessions"] = google_adk_sessions_mod


def _make_shared_runtime_stub() -> None:
    """Register shared_runtime stub with extract_message_text."""
    if "shared_runtime" not in sys.modules:
        mod = ModuleType("shared_runtime")

        def _extract_message_text(ctx) -> str:
            parts = getattr(getattr(ctx, "message", None), "parts", None)
            if parts is None:
                parts = ctx
            texts = []
            for p in parts or []:
                t = getattr(p, "text", None) or getattr(
                    getattr(p, "root", None), "text", None
                ) or ""
                if t:
                    texts.append(t)
            return " ".join(texts).strip()

        mod.extract_message_text = _extract_message_text
        sys.modules["shared_runtime"] = mod


def _make_adapter_base_stub() -> None:
    """Register adapter_base stub in sys.modules."""
    if "adapter_base" not in sys.modules:
        mod = ModuleType("adapter_base")
        from dataclasses import dataclass, field
        from abc import ABC, abstractmethod

        @dataclass
        class AdapterConfig:
            model: str = "google:gemini-2.0-flash"
            system_prompt: str | None = None
            tools: list = field(default_factory=list)
            runtime_config: dict = field(default_factory=dict)
            config_path: str = "/configs"
            workspace_id: str = ""
            prompt_files: list = field(default_factory=list)
            a2a_port: int = 8000
            heartbeat: object = None

        class BaseAdapter(ABC):
            @staticmethod
            @abstractmethod
            def name() -> str: ...  # pragma: no cover

            @staticmethod
            @abstractmethod
            def display_name() -> str: ...  # pragma: no cover

            @staticmethod
            @abstractmethod
            def description() -> str: ...  # pragma: no cover

            @staticmethod
            def get_config_schema() -> dict:
                return {}

            def memory_filename(self) -> str:
                return "CLAUDE.md"

            def register_tool_hook(self, name, fn): return None  # noqa

            async def transcript_lines(self, since=0, limit=100): return {"supported": False}  # noqa

            def register_subagent_hook(self, name, spec): return None  # noqa

            def append_to_memory_hook(self, config, filename, content): pass  # noqa

            async def install_plugins_via_registry(self, config, plugins): return []  # noqa

            async def inject_plugins(self, config, plugins):
                await self.install_plugins_via_registry(config, plugins)

            async def _common_setup(self, config):
                from types import SimpleNamespace
                return SimpleNamespace(
                    system_prompt="mocked system prompt",
                    loaded_skills=[],
                    langchain_tools=[],
                    is_coordinator=False,
                    children=[],
                )

            @abstractmethod
            async def setup(self, config) -> None: ...  # pragma: no cover

            @abstractmethod
            async def create_executor(self, config): ...  # pragma: no cover

        mod.AdapterConfig = AdapterConfig
        mod.BaseAdapter = BaseAdapter
        mod.SetupResult = None
        sys.modules["adapter_base"] = mod


# Install all stubs before importing the module under test
# Order matters: a2a must be stubbed before adapter.py is imported so that
# `from a2a.utils import new_agent_text_message` resolves to the passthrough.
_make_a2a_stubs()
_make_google_adk_stubs()
_make_shared_runtime_stub()
_make_adapter_base_stub()

# Now safe to import the adapter
import sys as _sys
import os as _os
_adapter_dir = _os.path.dirname(_os.path.abspath(__file__))
if _adapter_dir not in _sys.path:
    _sys.path.insert(0, _adapter_dir)

from adapter import (  # noqa: E402
    Adapter,
    GoogleADKA2AExecutor,
    GoogleADKAdapter,
    MissingContent,
    _DEFAULT_AGENT_NAME,
    _DEFAULT_MAX_OUTPUT_TOKENS,
    _DEFAULT_TEMPERATURE,
    _NO_RESPONSE_MSG,
    _NO_TEXT_MSG,
)


# ---------------------------------------------------------------------------
# Fixtures and helpers
# ---------------------------------------------------------------------------


def _make_context(text: str, context_id: str = "ctx-test") -> MagicMock:
    """Return a mock RequestContext with the given text in message.parts."""
    part = MagicMock()
    part.text = text
    ctx = MagicMock()
    ctx.message.parts = [part]
    ctx.context_id = context_id
    return ctx


def _make_empty_context() -> MagicMock:
    """Return a context whose message parts contain no text."""
    part = MagicMock(spec=[])
    part.root = MagicMock(spec=[])
    ctx = MagicMock()
    ctx.message.parts = [part]
    ctx.context_id = "ctx-empty"
    return ctx


def _make_event(is_final: bool, text: str | None = None) -> MagicMock:
    """Build a mock ADK Event that optionally is a final response."""
    event = MagicMock()
    event.is_final_response = MagicMock(return_value=is_final)
    if text is not None:
        part = MagicMock()
        part.text = text
        event.response = MagicMock()
        event.response.content = MagicMock()
        event.response.content.parts = [part]
    else:
        event.response = None
    return event


async def _async_gen(*events):
    """Yield events one by one as an async generator."""
    for e in events:
        yield e


def _make_runner(events=None) -> MagicMock:
    """Return a mock Runner whose run_async yields the given events."""
    runner = MagicMock()
    runner.session_service = AsyncMock()
    runner.session_service.get_session = AsyncMock(return_value=None)
    runner.session_service.create_session = AsyncMock(return_value={"id": "s1"})
    evts = events or []
    runner.run_async = MagicMock(return_value=_async_gen(*evts))
    return runner


def _make_executor(
    model: str = "gemini-2.0-flash",
    system_prompt: str | None = "You are helpful.",
    runner: MagicMock | None = None,
) -> GoogleADKA2AExecutor:
    """Create a GoogleADKA2AExecutor with an injected mock runner."""
    return GoogleADKA2AExecutor(
        model=model,
        system_prompt=system_prompt,
        _runner=runner or _make_runner(),
    )


def _make_adapter_config(**kwargs) -> object:
    """Return an AdapterConfig with sensible defaults."""
    from adapter_base import AdapterConfig
    defaults = dict(
        model="google:gemini-2.0-flash",
        system_prompt="Test prompt.",
        runtime_config={},
        workspace_id="ws-test",
    )
    defaults.update(kwargs)
    return AdapterConfig(**defaults)


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------


def test_default_agent_name():
    assert _DEFAULT_AGENT_NAME == "molecule-adk-agent"


def test_default_max_output_tokens():
    assert _DEFAULT_MAX_OUTPUT_TOKENS == 8192


def test_default_temperature():
    assert _DEFAULT_TEMPERATURE == 1.0


def test_no_text_msg_constant():
    assert "no text" in _NO_TEXT_MSG.lower()


def test_no_response_msg_constant():
    assert "no response" in _NO_RESPONSE_MSG.lower()


# ---------------------------------------------------------------------------
# MissingContent sentinel
# ---------------------------------------------------------------------------


def test_missing_content_has_empty_parts():
    mc = MissingContent()
    assert mc.parts == []


# ---------------------------------------------------------------------------
# GoogleADKA2AExecutor — construction
# ---------------------------------------------------------------------------


def test_constructor_stores_fields():
    runner = _make_runner()
    executor = GoogleADKA2AExecutor(
        model="gemini-1.5-pro",
        system_prompt="Hello",
        agent_name="my-agent",
        max_output_tokens=4096,
        temperature=0.5,
        _runner=runner,
    )
    assert executor.model == "gemini-1.5-pro"
    assert executor.system_prompt == "Hello"
    assert executor.agent_name == "my-agent"
    assert executor.max_output_tokens == 4096
    assert executor.temperature == 0.5
    assert executor._runner is runner
    assert executor._sessions_created == set()


def test_constructor_defaults():
    executor = GoogleADKA2AExecutor(model="gemini-2.0-flash", _runner=_make_runner())
    assert executor.system_prompt is None
    assert executor.agent_name == _DEFAULT_AGENT_NAME
    assert executor.max_output_tokens == _DEFAULT_MAX_OUTPUT_TOKENS
    assert executor.temperature == _DEFAULT_TEMPERATURE
    assert executor._heartbeat is None


def test_constructor_uses_injected_runner():
    stub = MagicMock()
    stub.session_service = MagicMock()
    executor = GoogleADKA2AExecutor(model="gemini-2.0-flash", _runner=stub)
    assert executor._runner is stub


# ---------------------------------------------------------------------------
# GoogleADKA2AExecutor — _extract_text
# ---------------------------------------------------------------------------


def test_extract_text_returns_message_text():
    executor = _make_executor()
    ctx = _make_context("Hello world")
    result = executor._extract_text(ctx)
    assert result == "Hello world"


def test_extract_text_empty_context():
    executor = _make_executor()
    ctx = _make_empty_context()
    result = executor._extract_text(ctx)
    assert result == ""


# ---------------------------------------------------------------------------
# GoogleADKA2AExecutor — _build_content
# ---------------------------------------------------------------------------


def test_build_content_creates_content_object():
    executor = _make_executor()
    content = executor._build_content("test message")
    assert content.role == "user"
    assert len(content.parts) == 1
    assert content.parts[0].text == "test message"


def test_build_content_empty_string():
    executor = _make_executor()
    content = executor._build_content("")
    assert content.parts[0].text == ""


# ---------------------------------------------------------------------------
# GoogleADKA2AExecutor — _ensure_session
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_ensure_session_creates_when_not_exists():
    runner = _make_runner()
    runner.session_service.get_session = AsyncMock(return_value=None)
    executor = GoogleADKA2AExecutor(
        model="gemini-2.0-flash", agent_name="test-agent", _runner=runner
    )
    await executor._ensure_session("session-1", "user-1")
    runner.session_service.create_session.assert_called_once_with(
        app_name="test-agent",
        user_id="user-1",
        session_id="session-1",
    )
    assert "session-1" in executor._sessions_created


@pytest.mark.asyncio
async def test_ensure_session_skips_if_already_tracked():
    runner = _make_runner()
    executor = GoogleADKA2AExecutor(
        model="gemini-2.0-flash", _runner=runner
    )
    executor._sessions_created.add("session-x")
    await executor._ensure_session("session-x", "user-1")
    # Neither get_session nor create_session should be called
    runner.session_service.get_session.assert_not_called()
    runner.session_service.create_session.assert_not_called()


@pytest.mark.asyncio
async def test_ensure_session_skips_create_when_existing():
    runner = _make_runner()
    runner.session_service.get_session = AsyncMock(return_value={"id": "s1"})
    executor = GoogleADKA2AExecutor(
        model="gemini-2.0-flash", agent_name="test-agent", _runner=runner
    )
    await executor._ensure_session("session-existing", "user-1")
    runner.session_service.create_session.assert_not_called()
    assert "session-existing" in executor._sessions_created


# ---------------------------------------------------------------------------
# GoogleADKA2AExecutor — execute: happy path
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_execute_returns_response_text():
    event = _make_event(is_final=True, text="The answer is 42.")
    runner = _make_runner(events=[event])
    executor = _make_executor(runner=runner)

    ctx = _make_context("What is 6×7?")
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    eq.enqueue_event.assert_called_once_with("The answer is 42.")


@pytest.mark.asyncio
async def test_execute_concatenates_multiple_final_parts():
    part1 = MagicMock()
    part1.text = "Hello "
    part2 = MagicMock()
    part2.text = "world"
    event = MagicMock()
    event.is_final_response = MagicMock(return_value=True)
    event.response = MagicMock()
    event.response.content = MagicMock()
    event.response.content.parts = [part1, part2]

    runner = _make_runner(events=[event])
    executor = _make_executor(runner=runner)

    ctx = _make_context("Hi")
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    eq.enqueue_event.assert_called_once_with("Hello world")


@pytest.mark.asyncio
async def test_execute_skips_non_final_events():
    non_final = _make_event(is_final=False, text="intermediate")
    final = _make_event(is_final=True, text="final answer")
    runner = _make_runner(events=[non_final, final])
    executor = _make_executor(runner=runner)

    ctx = _make_context("question")
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    enqueued = eq.enqueue_event.call_args[0][0]
    assert enqueued == "final answer"


@pytest.mark.asyncio
async def test_execute_fallback_when_no_final_response_events():
    non_final = _make_event(is_final=False)
    runner = _make_runner(events=[non_final])
    executor = _make_executor(runner=runner)

    ctx = _make_context("hello")
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    eq.enqueue_event.assert_called_once_with(_NO_RESPONSE_MSG)


@pytest.mark.asyncio
async def test_execute_fallback_when_response_is_none():
    event = MagicMock()
    event.is_final_response = MagicMock(return_value=True)
    event.response = None  # no response object

    runner = _make_runner(events=[event])
    executor = _make_executor(runner=runner)

    ctx = _make_context("ping")
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    eq.enqueue_event.assert_called_once_with(_NO_RESPONSE_MSG)


@pytest.mark.asyncio
async def test_execute_fallback_when_parts_have_no_text():
    part = MagicMock()
    part.text = None  # no text on the part
    event = MagicMock()
    event.is_final_response = MagicMock(return_value=True)
    event.response = MagicMock()
    event.response.content = MagicMock()
    event.response.content.parts = [part]

    runner = _make_runner(events=[event])
    executor = _make_executor(runner=runner)

    ctx = _make_context("ping")
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    eq.enqueue_event.assert_called_once_with(_NO_RESPONSE_MSG)


@pytest.mark.asyncio
async def test_execute_fallback_when_response_content_is_none():
    event = MagicMock()
    event.is_final_response = MagicMock(return_value=True)
    event.response = MagicMock()
    event.response.content = None  # content is None → MissingContent sentinel

    runner = _make_runner(events=[event])
    executor = _make_executor(runner=runner)

    ctx = _make_context("ping")
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    eq.enqueue_event.assert_called_once_with(_NO_RESPONSE_MSG)


@pytest.mark.asyncio
async def test_execute_uses_context_id_as_session_id():
    event = _make_event(is_final=True, text="ok")
    runner = _make_runner(events=[event])
    executor = _make_executor(runner=runner)

    ctx = _make_context("hello", context_id="ctx-abc-123")
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    runner.run_async.assert_called_once()
    call_kwargs = runner.run_async.call_args[1]
    assert call_kwargs["session_id"] == "ctx-abc-123"
    assert call_kwargs["user_id"] == "molecule-user"


@pytest.mark.asyncio
async def test_execute_falls_back_to_default_session_id_when_context_id_is_none():
    event = _make_event(is_final=True, text="ok")
    runner = _make_runner(events=[event])
    executor = _make_executor(runner=runner)

    ctx = _make_context("hello")
    ctx.context_id = None  # override
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    call_kwargs = runner.run_async.call_args[1]
    assert call_kwargs["session_id"] == "default-session"


# ---------------------------------------------------------------------------
# GoogleADKA2AExecutor — execute: empty input
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_execute_empty_input_returns_error():
    runner = _make_runner()
    executor = _make_executor(runner=runner)

    ctx = _make_empty_context()
    eq = AsyncMock()
    await executor.execute(ctx, eq)

    eq.enqueue_event.assert_called_once_with(_NO_TEXT_MSG)
    runner.run_async.assert_not_called()


# ---------------------------------------------------------------------------
# GoogleADKA2AExecutor — execute: error handling
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_execute_api_error_returns_sanitized_message():
    runner = _make_runner()

    class _FakeAPIError(Exception):
        pass

    async def _raise(*args, **kwargs):
        raise _FakeAPIError("api_key=secret token_limit_exceeded")
        yield  # make it an async generator

    runner.run_async = MagicMock(return_value=_raise())
    executor = _make_executor(runner=runner)

    eq = AsyncMock()
    await executor.execute(_make_context("hello"), eq)

    enqueued = eq.enqueue_event.call_args[0][0]
    assert enqueued == "Agent error: _FakeAPIError"
    assert "secret" not in enqueued


@pytest.mark.asyncio
async def test_execute_api_error_is_logged(caplog):
    import logging

    runner = _make_runner()

    async def _raise(*args, **kwargs):
        raise ValueError("bad request")
        yield  # make it an async generator

    runner.run_async = MagicMock(return_value=_raise())
    executor = _make_executor(runner=runner)

    with caplog.at_level(logging.ERROR, logger="adapter"):
        await executor.execute(_make_context("hello"), AsyncMock())

    assert any("execution error" in r.message.lower() for r in caplog.records)


# ---------------------------------------------------------------------------
# GoogleADKA2AExecutor — cancel
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_cancel_emits_canceled_event():
    executor = _make_executor()

    import a2a.types as a2a_types

    class _TaskState:
        canceled = "canceled"

    class _TaskStatus:
        def __init__(self, state):
            self.state = state

    class _TaskStatusUpdateEvent:
        def __init__(self, status, final):
            self.status = status
            self.final = final

    a2a_types.TaskState = _TaskState
    a2a_types.TaskStatus = _TaskStatus
    a2a_types.TaskStatusUpdateEvent = _TaskStatusUpdateEvent

    eq = AsyncMock()
    ctx = MagicMock()
    await executor.cancel(ctx, eq)

    eq.enqueue_event.assert_called_once()
    event = eq.enqueue_event.call_args[0][0]
    assert isinstance(event, _TaskStatusUpdateEvent)
    assert event.status.state == "canceled"
    assert event.final is True


# ---------------------------------------------------------------------------
# GoogleADKAdapter — identity methods
# ---------------------------------------------------------------------------


def test_adapter_name():
    assert GoogleADKAdapter.name() == "google-adk"


def test_adapter_display_name():
    assert "Google ADK" in GoogleADKAdapter.display_name()


def test_adapter_description():
    desc = GoogleADKAdapter.description()
    assert "ADK" in desc or "Google" in desc


def test_adapter_get_config_schema():
    schema = GoogleADKAdapter.get_config_schema()
    assert schema["type"] == "object"
    assert "agent_name" in schema["properties"]
    assert "max_output_tokens" in schema["properties"]
    assert "temperature" in schema["properties"]


# ---------------------------------------------------------------------------
# GoogleADKAdapter — setup
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_setup_succeeds_with_api_key(monkeypatch):
    monkeypatch.setenv("GOOGLE_API_KEY", "fake-api-key")
    monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False)

    adapter = GoogleADKAdapter()
    config = _make_adapter_config()

    await adapter.setup(config)

    assert adapter._setup_result is not None
    assert adapter._setup_result.system_prompt == "mocked system prompt"


@pytest.mark.asyncio
async def test_setup_succeeds_with_vertex_ai(monkeypatch):
    monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
    monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "1")

    adapter = GoogleADKAdapter()
    config = _make_adapter_config()

    await adapter.setup(config)

    assert adapter._setup_result is not None


@pytest.mark.asyncio
async def test_setup_succeeds_with_vertex_ai_true_string(monkeypatch):
    monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
    monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "True")

    adapter = GoogleADKAdapter()
    config = _make_adapter_config()

    await adapter.setup(config)
    assert adapter._setup_result is not None


@pytest.mark.asyncio
async def test_setup_raises_without_credentials(monkeypatch):
    monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
    monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False)

    adapter = GoogleADKAdapter()
    config = _make_adapter_config()

    with pytest.raises(RuntimeError, match="GOOGLE_API_KEY"):
        await adapter.setup(config)


# ---------------------------------------------------------------------------
# GoogleADKAdapter — create_executor
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_create_executor_strips_google_prefix(monkeypatch):
    monkeypatch.setenv("GOOGLE_API_KEY", "key")
    adapter = GoogleADKAdapter()
    config = _make_adapter_config(model="google:gemini-2.0-flash")
    await adapter.setup(config)

    executor = await adapter.create_executor(config)
    assert executor.model == "gemini-2.0-flash"


@pytest.mark.asyncio
async def test_create_executor_no_prefix_passthrough(monkeypatch):
    monkeypatch.setenv("GOOGLE_API_KEY", "key")
    adapter = GoogleADKAdapter()
    config = _make_adapter_config(model="gemini-1.5-pro")
    await adapter.setup(config)

    executor = await adapter.create_executor(config)
    assert executor.model == "gemini-1.5-pro"


@pytest.mark.asyncio
async def test_create_executor_uses_setup_system_prompt(monkeypatch):
    monkeypatch.setenv("GOOGLE_API_KEY", "key")
    adapter = GoogleADKAdapter()
    config = _make_adapter_config()
    await adapter.setup(config)

    executor = await adapter.create_executor(config)
    assert executor.system_prompt == "mocked system prompt"


@pytest.mark.asyncio
async def test_create_executor_runtime_config_overrides(monkeypatch):
    monkeypatch.setenv("GOOGLE_API_KEY", "key")
    adapter = GoogleADKAdapter()
    config = _make_adapter_config(
        runtime_config={
            "agent_name": "custom-agent",
            "max_output_tokens": 512,
            "temperature": 0.3,
        }
    )
    await adapter.setup(config)

    executor = await adapter.create_executor(config)
    assert executor.agent_name == "custom-agent"
    assert executor.max_output_tokens == 512
    assert executor.temperature == 0.3


@pytest.mark.asyncio
async def test_create_executor_defaults_without_runtime_config(monkeypatch):
    monkeypatch.setenv("GOOGLE_API_KEY", "key")
    adapter = GoogleADKAdapter()
    config = _make_adapter_config(runtime_config={})
    await adapter.setup(config)

    executor = await adapter.create_executor(config)
    assert executor.agent_name == _DEFAULT_AGENT_NAME
    assert executor.max_output_tokens == _DEFAULT_MAX_OUTPUT_TOKENS
    assert executor.temperature == _DEFAULT_TEMPERATURE


@pytest.mark.asyncio
async def test_create_executor_without_setup_uses_config_system_prompt(monkeypatch):
    """create_executor without prior setup falls back to config.system_prompt."""
    monkeypatch.setenv("GOOGLE_API_KEY", "key")
    adapter = GoogleADKAdapter()
    config = _make_adapter_config(system_prompt="fallback prompt")
    # Intentionally skip setup() — _setup_result remains None

    executor = await adapter.create_executor(config)
    assert executor.system_prompt == "fallback prompt"


@pytest.mark.asyncio
async def test_create_executor_without_setup_no_system_prompt(monkeypatch):
    """create_executor without setup and no system_prompt → empty string."""
    monkeypatch.setenv("GOOGLE_API_KEY", "key")
    adapter = GoogleADKAdapter()
    config = _make_adapter_config(system_prompt=None)
    # Skip setup()

    executor = await adapter.create_executor(config)
    assert executor.system_prompt == ""


@pytest.mark.asyncio
async def test_create_executor_heartbeat_passed(monkeypatch):
    monkeypatch.setenv("GOOGLE_API_KEY", "key")
    adapter = GoogleADKAdapter()
    heartbeat = MagicMock()
    config = _make_adapter_config(heartbeat=heartbeat)
    await adapter.setup(config)

    executor = await adapter.create_executor(config)
    assert executor._heartbeat is heartbeat


# ---------------------------------------------------------------------------
# Adapter alias
# ---------------------------------------------------------------------------


def test_adapter_alias_is_google_adk_adapter():
    assert Adapter is GoogleADKAdapter
