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

"""Guard tests for the release-cut dependency contract.

These tests pin the public dependency surface so that release-blocking
regressions documented in the bare-install audit cannot silently re-emerge:

* ``packaging`` MUST be declared in main deps (used at import-time by
  ``utils/model_name_utils.py`` and ``cli/cli_deploy.py``; reachable from
  ``from google.adk import Runner`` and from ``adk --help``).
* ``ValidationError`` in ``environment_simulation_config`` MUST come from
  ``pydantic`` (which always installs alongside the package), NOT from the
  undeclared ``pydantic_core``.
"""

from __future__ import annotations

import importlib.util
from pathlib import Path

try:
  import tomllib
except ImportError:
  import tomli as tomllib

import pytest


def _find_pyproject() -> Path:
  """Locates pyproject.toml by walking up from this file's directory.

  Handles layouts where pyproject.toml is at an ancestor directory as well as
  layouts where it lives in a sibling build directory next to the package. The
  test tree may be symlinked, so the walk avoids ``.resolve()``.
  """
  start = Path(__file__).parent
  for candidate in [start, *start.parents]:
    direct = candidate / 'pyproject.toml'
    if direct.is_file():
      return direct
    try:
      children = sorted(p for p in candidate.iterdir() if p.is_dir())
    except OSError:
      continue
    for child in children:
      sibling = child / 'pyproject.toml'
      if sibling.is_file():
        return sibling
  raise FileNotFoundError(
      f'Could not find pyproject.toml walking up from {start}.'
  )


_PYPROJECT_PATH = _find_pyproject()


@pytest.fixture(scope='module')
def pyproject() -> dict:
  """Parses the project's pyproject.toml exactly once for the module."""
  with _PYPROJECT_PATH.open('rb') as fh:
    return tomllib.load(fh)


def _requirement_names(requirements: list[str]) -> set[str]:
  """Returns the lowercased PEP 508 distribution names from ``requirements``.

  Strips extras specifiers, version specifiers, and environment markers so the
  caller can do exact-name membership checks.
  """
  names: set[str] = set()
  for req in requirements:
    # Drop everything after a marker, version specifier, or extras block.
    head = req.split(';', 1)[0].strip()
    for sep in ('[', '>', '<', '=', '!', '~', ' '):
      head = head.split(sep, 1)[0]
    names.add(head.strip().lower())
  return names


def test_main_deps_include_packaging(pyproject: dict) -> None:
  """``packaging`` is imported unguarded by core ADK; it must be a main dep."""
  main_deps = _requirement_names(pyproject['project']['dependencies'])
  assert 'packaging' in main_deps, (
      'packaging must be declared in [project] dependencies because '
      'src/google/adk/utils/model_name_utils.py and '
      'src/google/adk/cli/cli_deploy.py import it unguarded at module top '
      'level. Without this declaration, `pip install google-adk` is one '
      'transitive resolver change away from breaking on `import google.adk`.'
  )


def test_environment_simulation_config_imports_validation_error_from_pydantic() -> (
    None
):
  """The ValidationError used by the config module must come from pydantic.

  pydantic-core is undeclared; importing from it directly is fragile. pydantic
  re-exports ValidationError, so use that.
  """
  # Use importlib to locate the source file so the test is independent of the
  # on-disk package layout.
  spec = importlib.util.find_spec(
      'google.adk.tools.environment_simulation.environment_simulation_config'
  )
  assert (
      spec is not None and spec.origin is not None
  ), 'environment_simulation_config module is not importable.'
  source_path = Path(spec.origin)
  source = source_path.read_text(encoding='utf-8')
  assert 'from pydantic import ValidationError' in source, (
      'environment_simulation_config.py must import ValidationError from '
      'pydantic, not pydantic_core. pydantic_core is undeclared as a main '
      'dep and pydantic re-exports the same class.'
  )
  assert 'from pydantic_core import ValidationError' not in source, (
      'environment_simulation_config.py must not import ValidationError '
      'from pydantic_core (undeclared dep).'
  )


def test_injection_config_validation_raises_pydantic_validation_error() -> None:
  """Behavioral check: invalid config raises the pydantic ValidationError."""
  # Local import keeps this test focused on the post-fix code path and
  # surfaces ImportError clearly if the module's import block regresses.
  from google.adk.tools.environment_simulation.environment_simulation_config import InjectedError
  from pydantic import ValidationError

  with pytest.raises(ValidationError):
    # Both required fields missing — pydantic must reject the construction.
    InjectedError()  # type: ignore[call-arg]
