# 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.

"""Integration tests for grounding metadata preservation in SSE streaming.

Verifies that grounding_metadata from VertexAiSearchTool reaches the final
non-partial event in both progressive and non-progressive SSE streaming modes.

Prerequisites:
  - GOOGLE_CLOUD_PROJECT env var set to a GCP project with Vertex AI enabled
  - Discovery Engine API enabled (discoveryengine.googleapis.com)
  - Authenticated via `gcloud auth application-default login`

Usage:
  GOOGLE_CLOUD_PROJECT=my-project pytest
  tests/integration/test_vertex_ai_search_grounding_streaming.py -v -s
"""

from __future__ import annotations

import json
import os
import time
import uuid

from google.adk.features._feature_registry import FeatureName
from google.adk.features._feature_registry import temporary_feature_override
from google.genai import types
import pytest

_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "")
_LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION", "global")
_COLLECTION = "default_collection"
_DATA_STORE_ID = f"adk-grounding-test-{uuid.uuid4().hex[:8]}"
_DATA_STORE_DISPLAY_NAME = "ADK Grounding Integration Test"
_MODEL = "gemini-2.5-flash"

_TEST_DOCUMENTS = (
    {
        "id": "doc-adk-overview",
        "title": "ADK Overview",
        "content": (
            "The Agent Development Kit (ADK) is an open-source framework by"
            " Google for building AI agents. ADK supports multi-agent"
            " architectures, tool use, and integrates with Gemini models."
            " ADK was first released in April 2025."
        ),
    },
    {
        "id": "doc-adk-tools",
        "title": "ADK Built-in Tools",
        "content": (
            "ADK provides built-in tools including VertexAiSearchTool for"
            " grounded search, GoogleSearchTool for web search, and"
            " CodeExecutionTool for running code. The VertexAiSearchTool"
            " returns grounding metadata with citations pointing to source"
            " documents."
        ),
    },
)


def _parent_path() -> str:
  return f"projects/{_PROJECT}/locations/{_LOCATION}/collections/{_COLLECTION}"


def _data_store_path() -> str:
  return f"{_parent_path()}/dataStores/{_DATA_STORE_ID}"


@pytest.fixture(scope="module")
def project_id():
  if not _PROJECT:
    pytest.skip("GOOGLE_CLOUD_PROJECT env var not set")
  return _PROJECT


@pytest.fixture(scope="module")
def data_store_resource(project_id) -> str:
  """Create a Vertex AI Search data store with test documents."""
  from google.api_core.exceptions import AlreadyExists
  from google.cloud import discoveryengine_v1beta as discoveryengine

  ds_client = discoveryengine.DataStoreServiceClient()
  doc_client = discoveryengine.DocumentServiceClient()

  # Create data store
  try:
    request = discoveryengine.CreateDataStoreRequest(
        parent=_parent_path(),
        data_store=discoveryengine.DataStore(
            display_name=_DATA_STORE_DISPLAY_NAME,
            industry_vertical=discoveryengine.IndustryVertical.GENERIC,
            solution_types=[discoveryengine.SolutionType.SOLUTION_TYPE_SEARCH],
            content_config=discoveryengine.DataStore.ContentConfig.NO_CONTENT,
        ),
        data_store_id=_DATA_STORE_ID,
    )
    operation = ds_client.create_data_store(request=request)
    print(f"\nCreating data store '{_DATA_STORE_ID}'...")
    operation.result(timeout=120)
    print("Data store created.")
  except AlreadyExists:
    print(f"\nData store '{_DATA_STORE_ID}' already exists, reusing.")

  # Ingest test documents
  branch = f"{_data_store_path()}/branches/default_branch"
  for doc_data in _TEST_DOCUMENTS:
    json_data = json.dumps({
        "title": doc_data["title"],
        "description": doc_data["content"],
    })
    doc = discoveryengine.Document(
        id=doc_data["id"],
        json_data=json_data,
    )
    try:
      doc_client.create_document(
          parent=branch,
          document=doc,
          document_id=doc_data["id"],
      )
      print(f"  Created document: {doc_data['id']}")
    except AlreadyExists:
      doc_client.update_document(
          document=discoveryengine.Document(
              name=f"{branch}/documents/{doc_data['id']}",
              json_data=json_data,
          ),
      )
      print(f"  Updated document: {doc_data['id']}")

  print("Waiting 5s for indexing...")
  time.sleep(5)

  yield _data_store_path()

  # Cleanup — best-effort, ignore errors from Discovery Engine LRO
  try:
    operation = ds_client.delete_data_store(name=_data_store_path())
    operation.result(timeout=120)
    print(f"\nDeleted data store '{_DATA_STORE_ID}'.")
  except Exception as e:
    print(f"\nFailed to delete data store '{_DATA_STORE_ID}': {e}")


class TestIntegrationVertexAiSearchGrounding:
  """Integration tests hitting real Vertex AI with VertexAiSearchTool."""

  @pytest.mark.parametrize("llm_backend", ["VERTEX"], indirect=True)
  @pytest.mark.parametrize(
      "progressive_sse, label",
      [
          (True, "Progressive SSE"),
          (False, "Non-Progressive SSE"),
      ],
  )
  @pytest.mark.asyncio
  async def test_grounding_metadata_with_sse_streaming(
      self, project_id, data_store_resource, progressive_sse, label
  ):
    """Verifies grounding_metadata in SSE streaming modes."""
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.tools.vertex_ai_search_tool import VertexAiSearchTool

    agent = LlmAgent(
        name="test_agent",
        model=_MODEL,
        tools=[VertexAiSearchTool(data_store_id=data_store_resource)],
        instruction="Answer questions using the search tool.",
    )

    with temporary_feature_override(
        FeatureName.PROGRESSIVE_SSE_STREAMING, progressive_sse
    ):
      all_events, saved_events = await self._run_agent_streaming(
          agent, project_id
      )

    self._report_events(label, all_events, saved_events)

    saved_with_grounding = [e for e in saved_events if e["has_grounding"]]
    assert (
        saved_with_grounding
    ), f"No saved (non-partial) events have grounding_metadata with {label}."

  @pytest.mark.parametrize("llm_backend", ["VERTEX"], indirect=True)
  @pytest.mark.asyncio
  async def test_grounding_metadata_without_streaming(
      self, project_id, data_store_resource
  ):
    """Without streaming, grounding_metadata should always be present."""
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.agents.run_config import RunConfig
    from google.adk.agents.run_config import StreamingMode
    from google.adk.runners import Runner
    from google.adk.sessions.in_memory_session_service import InMemorySessionService
    from google.adk.tools.vertex_ai_search_tool import VertexAiSearchTool
    from google.adk.utils.context_utils import Aclosing

    agent = LlmAgent(
        name="test_agent",
        model=_MODEL,
        tools=[VertexAiSearchTool(data_store_id=data_store_resource)],
        instruction="Answer questions using the search tool.",
    )

    session_service = InMemorySessionService()
    runner = Runner(
        app_name="test_app",
        agent=agent,
        session_service=session_service,
    )
    session = await session_service.create_session(
        app_name="test_app", user_id="test_user"
    )

    run_config = RunConfig(streaming_mode=StreamingMode.NONE)
    events = []
    async with Aclosing(
        runner.run_async(
            user_id="test_user",
            session_id=session.id,
            new_message=types.Content(
                role="user",
                parts=[
                    types.Part.from_text(
                        text="What built-in tools does ADK provide?"
                    )
                ],
            ),
            run_config=run_config,
        )
    ) as agen:
      async for event in agen:
        events.append({
            "author": event.author,
            "partial": event.partial,
            "has_grounding": event.grounding_metadata is not None,
            "has_content": bool(event.content and event.content.parts),
        })

    print("\n=== No Streaming ===")
    for i, e in enumerate(events):
      print(
          f"  Event {i}: author={e['author']}, partial={e['partial']},"
          f" grounding={e['has_grounding']}, content={e['has_content']}"
      )

    model_events = [e for e in events if e["author"] == "test_agent"]
    with_grounding = [e for e in model_events if e["has_grounding"]]
    assert (
        with_grounding
    ), "No events have grounding_metadata even without streaming."

  async def _run_agent_streaming(self, agent, project_id):
    from google.adk.agents.run_config import RunConfig
    from google.adk.agents.run_config import StreamingMode
    from google.adk.runners import Runner
    from google.adk.sessions.in_memory_session_service import InMemorySessionService
    from google.adk.utils.context_utils import Aclosing

    session_service = InMemorySessionService()
    runner = Runner(
        app_name="test_app",
        agent=agent,
        session_service=session_service,
    )
    session = await session_service.create_session(
        app_name="test_app", user_id="test_user"
    )

    run_config = RunConfig(streaming_mode=StreamingMode.SSE)
    all_events = []
    async with Aclosing(
        runner.run_async(
            user_id="test_user",
            session_id=session.id,
            new_message=types.Content(
                role="user",
                parts=[
                    types.Part.from_text(
                        text="What is ADK and when was it first released?"
                    )
                ],
            ),
            run_config=run_config,
        )
    ) as agen:
      async for event in agen:
        all_events.append({
            "author": event.author,
            "partial": event.partial,
            "has_grounding": event.grounding_metadata is not None,
            "has_content": bool(event.content and event.content.parts),
        })

    saved_events = [e for e in all_events if e["partial"] is not True]
    return all_events, saved_events

  def _report_events(self, label, all_events, saved_events):
    print(f"\n=== {label} — All Events ===")
    for i, e in enumerate(all_events):
      print(
          f"  Event {i}: author={e['author']}, partial={e['partial']},"
          f" grounding={e['has_grounding']},"
          f" content={e['has_content']}"
      )
    print(f"\n=== {label} — Saved (non-partial) Events ===")
    for i, e in enumerate(saved_events):
      print(
          f"  Event {i}: author={e['author']}, partial={e['partial']},"
          f" grounding={e['has_grounding']},"
          f" content={e['has_content']}"
      )
    partial_with_grounding = [
        e for e in all_events if e["partial"] is True and e["has_grounding"]
    ]
    if partial_with_grounding:
      print(
          f"\n  NOTE: {len(partial_with_grounding)} partial event(s)"
          " had grounding_metadata but were NOT saved to session."
      )
