# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""E2E Integration Test for GCP Agent Identity Auth Provider two-legged OAuth Flow."""

import dataclasses
from typing import Any
from unittest import mock

from google.adk import Agent
from google.adk import Runner
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import _iam_connector_credentials_provider
from google.adk.integrations.agent_identity import GcpAuthProvider
from google.adk.integrations.agent_identity import GcpAuthProviderScheme
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.base_authenticated_tool import BaseAuthenticatedTool
from google.adk.tools.mcp_tool.mcp_tool import McpTool
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsRequest
from google.cloud.iamconnectorcredentials_v1alpha import RetrieveCredentialsResponse
from google.genai import types
from mcp.types import Tool as McpBaseTool
import pytest

from tests.unittests import testing_utils

DUMMY_TOKEN = "fake-gcp-2lo-token-123"
TEST_CONNECTOR_2LO = (
    "projects/test-project/locations/global/connectors/test-connector"
)


class DummyTool(BaseAuthenticatedTool):

  def __init__(self, auth_config: AuthConfig) -> None:
    super().__init__(
        name="dummy_tool",
        description="Dummy tool for testing 2LO.",
        auth_config=auth_config,
    )

  def _get_declaration(self) -> types.FunctionDeclaration:
    return types.FunctionDeclaration(
        name=self.name,
        description=self.description,
        parameters=types.Schema(
            type="OBJECT",
            properties={},
        ),
    )

  async def _run_async_impl(
      self, *, args: dict[str, Any] | None, tool_context: Any, credential: Any
  ) -> Any:
    # Return the token to prove the provider gave the expected credential
    if credential.http and credential.http.credentials:
      return credential.http.credentials.token
    if credential.oauth2 and credential.oauth2.access_token:
      return credential.oauth2.access_token
    return None


@dataclasses.dataclass
class _DummyOperation:
  done: bool = True
  error: Any = None
  metadata: Any = None
  response: Any = dataclasses.field(init=False)
  operation: Any = dataclasses.field(init=False)

  def __post_init__(self) -> None:
    self.response = mock.Mock()
    mock_credential = RetrieveCredentialsResponse(
        header="Authorization: Bearer", token=DUMMY_TOKEN
    )
    self.response.value = RetrieveCredentialsResponse.serialize(mock_credential)
    self.operation = self

  def HasField(self, field_name: str) -> bool:
    return getattr(self, field_name, None) is not None


# Mocked execution; pin to a single LLM backend to avoid duplicate runs.
@pytest.mark.parametrize("llm_backend", ["GOOGLE_AI"], indirect=True)
@pytest.mark.asyncio
async def test_gcp_agent_identity_2lo_gets_token() -> None:
  """Test the end-to-end flow fetching 2LO OAuth token from GCP Agent Identity credentials service."""

  # Clear registry to isolate tests
  CredentialManager._auth_provider_registry._providers.clear()

  # 1. Setup mocked GCP Client to return the fake Bearer token
  with mock.patch.object(
      _iam_connector_credentials_provider,
      "Client",
      autospec=True,
  ) as mock_client_cls:

    mock_operation = _DummyOperation()

    mock_client_cls.return_value.retrieve_credentials.return_value = (
        mock_operation
    )

    # 2. Configure Auth and DummyTool
    auth_scheme = GcpAuthProviderScheme(
        name=TEST_CONNECTOR_2LO,
        scopes=["test-scope"],
    )
    auth_config = AuthConfig(auth_scheme=auth_scheme)
    dummy_tool = DummyTool(auth_config=auth_config)

    # 3. Setup LLM, Agent, and Runner
    # We mock the LLM to just issue the tool call to 'dummy_tool'
    mock_model = testing_utils.MockModel.create(
        responses=[
            types.Part.from_function_call(name="dummy_tool", args={}),
            "Tool executed successfully.",
        ]
    )

    agent = Agent(
        name="test_agent",
        model=mock_model,
        instruction="You are an agent. Use the dummy_tool when needed.",
        tools=[dummy_tool],
    )

    runner = Runner(
        app_name="test_mcp_2lo_app",
        agent=agent,
        session_service=InMemorySessionService(),
        auto_create_session=True,
    )

    # 4. Register Auth Provider
    CredentialManager.register_auth_provider(GcpAuthProvider())

    # 5. Execute Flow
    event_list = []
    async for event in runner.run_async(
        user_id="test_user",
        session_id="test_session1",
        new_message=types.UserContent(
            parts=[types.Part(text="Get me the token.")]
        ),
    ):
      event_list.append(event)

    # 6. Assertions

    # Assert GCP Agent Identity client was invoked for credentials
    expected_request = RetrieveCredentialsRequest(
        connector=TEST_CONNECTOR_2LO,
        user_id="test_user",
        scopes=["test-scope"],
        continue_uri="",
        force_refresh=False,
    )
    mock_client_cls.return_value.retrieve_credentials.assert_called_once_with(
        expected_request
    )

    # 3 Events: Model FunctionCall -> Tool FunctionResponse -> Final LLM Text
    assert len(event_list) == 3
    last_event = event_list[-1]
    assert last_event.content.parts[0].text == "Tool executed successfully."

    # Validate that the mock model received the query and the tool callback
    requests = mock_model.requests
    # 2 Events: User Input -> Tool FunctionResponse
    assert len(requests) == 2

    # Extract the function response from the prompt payload sent to the LLM
    last_request = requests[-1]
    function_response = next(
        (
            p.function_response
            for p in last_request.contents[-1].parts
            if p.function_response
        ),
        None,
    )

    assert function_response.name == "dummy_tool"
    assert DUMMY_TOKEN in str(function_response.response)


@pytest.mark.parametrize("llm_backend", ["GOOGLE_AI"], indirect=True)
@pytest.mark.asyncio
async def test_gcp_agent_identity_2lo_sends_authorization_header_to_mcp_session(
    llm_backend: Any,
) -> None:
  """Ensures a 2LO token from GCP is correctly passed into the outbound MCP session headers."""
  CredentialManager._auth_provider_registry._providers.clear()
  CredentialManager.register_auth_provider(GcpAuthProvider())

  mock_operation = _DummyOperation()
  with mock.patch.object(
      _iam_connector_credentials_provider, "Client", autospec=True
  ) as mock_gcp:
    mock_gcp.return_value.retrieve_credentials.return_value = mock_operation

    mock_session_mgr = mock.AsyncMock()
    mock_session_mgr.create_session.return_value.call_tool.return_value = (
        mock.MagicMock()
    )

    mcp_tool = McpTool(
        mcp_tool=McpBaseTool(
            name="dummy_mcp",
            description="Dummy MCP tool for testing.",
            inputSchema={"type": "object", "properties": {}},
        ),
        mcp_session_manager=mock_session_mgr,
        auth_scheme=GcpAuthProviderScheme(
            name=TEST_CONNECTOR_2LO, scopes=["test-scope"]
        ),
    )

    agent = Agent(
        name="test_agent",
        model=testing_utils.MockModel.create(
            responses=[
                types.Part.from_function_call(name="dummy_mcp", args={}),
                "Tool executed successfully.",
            ]
        ),
        instruction="Use dummy_mcp tool.",
        tools=[mcp_tool],
    )

    async for _ in Runner(
        app_name="test_mcp_header_app",
        agent=agent,
        session_service=InMemorySessionService(),
        auto_create_session=True,
    ).run_async(
        user_id="test_user",
        session_id="session-id-2",
        new_message=types.UserContent(parts=[types.Part(text="Run tool.")]),
    ):
      pass

    mock_gcp.return_value.retrieve_credentials.assert_called_once_with(
        RetrieveCredentialsRequest(
            connector=TEST_CONNECTOR_2LO,
            user_id="test_user",
            scopes=["test-scope"],
            force_refresh=False,
        )
    )

    assert mock_session_mgr.create_session.call_args.kwargs.get("headers") == {
        "Authorization": f"Bearer {DUMMY_TOKEN}"
    }
