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

"""Unit tests for skill utilities."""

import builtins
import io
import sys
from unittest import mock
import zipfile

from google.adk.skills import list_skills_in_dir
from google.adk.skills import list_skills_in_gcs_dir as _list_skills_in_gcs_dir
from google.adk.skills import load_skill_from_dir as _load_skill_from_dir
from google.adk.skills import load_skill_from_gcs_dir as _load_skill_from_gcs_dir
from google.adk.skills._utils import _load_skill_from_zip_bytes
from google.adk.skills._utils import _read_skill_properties
from google.adk.skills._utils import _validate_skill_dir
import pytest


def test__load_skill_from_dir(tmp_path):
  """Tests loading a skill from a directory."""
  skill_dir = tmp_path / "test-skill"
  skill_dir.mkdir()

  skill_md_content = """---
name: test-skill
description: Test description
---
Test instructions
"""
  (skill_dir / "SKILL.md").write_text(skill_md_content)

  # Create references
  ref_dir = skill_dir / "references"
  ref_dir.mkdir()
  (ref_dir / "ref1.md").write_text("ref1 content")

  # Create assets
  assets_dir = skill_dir / "assets"
  assets_dir.mkdir()
  (assets_dir / "asset1.txt").write_text("asset1 content")

  # Create scripts
  scripts_dir = skill_dir / "scripts"
  scripts_dir.mkdir()
  (scripts_dir / "script1.sh").write_text("echo hello")

  skill = _load_skill_from_dir(skill_dir)

  assert skill.name == "test-skill"
  assert skill.description == "Test description"
  assert skill.instructions == "Test instructions"
  assert skill.resources.get_reference("ref1.md") == "ref1 content"
  assert skill.resources.get_asset("asset1.txt") == "asset1 content"
  assert skill.resources.get_script("script1.sh").src == "echo hello"


def test_allowed_tools_yaml_key(tmp_path):
  """Tests that allowed-tools YAML key loads correctly."""
  skill_dir = tmp_path / "my-skill"
  skill_dir.mkdir()

  skill_md = """---
name: my-skill
description: A skill
allowed-tools: "some-tool-*"
---
Instructions here
"""
  (skill_dir / "SKILL.md").write_text(skill_md)

  skill = _load_skill_from_dir(skill_dir)
  assert skill.frontmatter.allowed_tools == "some-tool-*"


def test_name_directory_mismatch(tmp_path):
  """Tests that name-directory mismatch raises ValueError."""
  skill_dir = tmp_path / "wrong-dir"
  skill_dir.mkdir()

  skill_md = """---
name: my-skill
description: A skill
---
Body
"""
  (skill_dir / "SKILL.md").write_text(skill_md)

  with pytest.raises(ValueError, match="does not match directory"):
    _load_skill_from_dir(skill_dir)


def test_validate_skill_dir_valid(tmp_path):
  """Tests validate_skill_dir with a valid skill."""
  skill_dir = tmp_path / "my-skill"
  skill_dir.mkdir()

  skill_md = """---
name: my-skill
description: A skill
---
Body
"""
  (skill_dir / "SKILL.md").write_text(skill_md)

  problems = _validate_skill_dir(skill_dir)
  assert problems == []


def test_validate_skill_dir_missing_dir(tmp_path):
  """Tests validate_skill_dir with missing directory."""
  problems = _validate_skill_dir(tmp_path / "nonexistent")
  assert len(problems) == 1
  assert "does not exist" in problems[0]


def test_validate_skill_dir_missing_skill_md(tmp_path):
  """Tests validate_skill_dir with missing SKILL.md."""
  skill_dir = tmp_path / "my-skill"
  skill_dir.mkdir()

  problems = _validate_skill_dir(skill_dir)
  assert len(problems) == 1
  assert "SKILL.md not found" in problems[0]


def test_validate_skill_dir_name_mismatch(tmp_path):
  """Tests validate_skill_dir catches name-directory mismatch."""
  skill_dir = tmp_path / "wrong-dir"
  skill_dir.mkdir()

  skill_md = """---
name: my-skill
description: A skill
---
Body
"""
  (skill_dir / "SKILL.md").write_text(skill_md)

  problems = _validate_skill_dir(skill_dir)
  assert any("does not match" in p for p in problems)


def test_validate_skill_dir_unknown_fields(tmp_path):
  """Tests validate_skill_dir detects unknown frontmatter fields."""
  skill_dir = tmp_path / "my-skill"
  skill_dir.mkdir()

  skill_md = """---
name: my-skill
description: A skill
unknown-field: something
---
Body
"""
  (skill_dir / "SKILL.md").write_text(skill_md)

  problems = _validate_skill_dir(skill_dir)
  assert any("Unknown frontmatter" in p for p in problems)


def test__read_skill_properties(tmp_path):
  """Tests read_skill_properties basic usage."""
  skill_dir = tmp_path / "my-skill"
  skill_dir.mkdir()

  skill_md = """---
name: my-skill
description: A cool skill
license: MIT
---
Body content
"""
  (skill_dir / "SKILL.md").write_text(skill_md)

  fm = _read_skill_properties(skill_dir)
  assert fm.name == "my-skill"
  assert fm.description == "A cool skill"
  assert fm.license == "MIT"


@mock.patch("google.cloud.storage.Client")
def test__list_skills_in_gcs_dir(mock_client_class):

  mock_client = mock.MagicMock()
  mock_client_class.return_value = mock_client
  mock_bucket = mock.MagicMock()
  mock_client.bucket.return_value = mock_bucket

  mock_iterator = mock.MagicMock()
  mock_iterator.prefixes = ["skills/my-skill/"]
  mock_bucket.list_blobs.return_value = mock_iterator

  mock_blob = mock.MagicMock()
  mock_blob.exists.return_value = True
  mock_blob.download_as_text.return_value = (
      "---\nname: my-skill\ndescription: A skill\n---\nBody"
  )
  mock_bucket.blob.return_value = mock_blob

  skills = _list_skills_in_gcs_dir("my-bucket", "skills/")
  assert "my-skill" in skills
  assert skills["my-skill"].name == "my-skill"


@mock.patch("google.cloud.storage.Client")
@mock.patch("logging.warning")
def test__list_skills_in_gcs_dir_skips_invalid(
    mock_logging_warning, mock_client_class
):
  mock_client = mock.MagicMock()
  mock_client_class.return_value = mock_client
  mock_bucket = mock.MagicMock()
  mock_client.bucket.return_value = mock_bucket

  mock_iterator = mock.MagicMock()
  mock_iterator.prefixes = ["skills/invalid-skill/", "skills/valid-skill/"]
  mock_bucket.list_blobs.return_value = mock_iterator

  def mock_blob_side_effect(path):
    m = mock.MagicMock()
    m.exists.return_value = True
    if "invalid-skill" in path:
      m.download_as_text.return_value = "invalid yaml content"
    else:
      m.download_as_text.return_value = (
          "---\nname: valid-skill\ndescription: A skill\n---\nBody"
      )
    return m

  mock_bucket.blob.side_effect = mock_blob_side_effect

  skills = _list_skills_in_gcs_dir("my-bucket", "skills/")
  assert "valid-skill" in skills
  assert "invalid-skill" not in skills

  # Verify warning was logged for the invalid skill
  mock_logging_warning.assert_called_once()
  args, _ = mock_logging_warning.call_args
  assert "Skipping invalid skill" in args[0]
  assert args[1] == "invalid-skill"
  assert args[2] == "my-bucket"


@mock.patch("google.cloud.storage.Client")
def test__load_skill_from_gcs_dir(mock_client_class):

  mock_client = mock.MagicMock()
  mock_client_class.return_value = mock_client
  mock_bucket = mock.MagicMock()
  mock_client.bucket.return_value = mock_bucket

  def mock_blob_side_effect(path):
    m = mock.MagicMock()
    if path.endswith("SKILL.md"):
      m.exists.return_value = True
      m.download_as_text.return_value = (
          "---\nname: my-skill\ndescription: Test description\n---\nTest"
          " instructions"
      )
    else:
      m.exists.return_value = False
    return m

  mock_bucket.blob.side_effect = mock_blob_side_effect

  # For resources
  def list_blobs_side_effect(prefix=None):
    if prefix.endswith("references/"):
      m = mock.MagicMock()
      m.name = prefix + "ref1.md"
      m.download_as_text.return_value = "ref1 content"
      return [m]
    return []

  mock_bucket.list_blobs.side_effect = list_blobs_side_effect

  skill = _load_skill_from_gcs_dir("my-bucket", "skills/my-skill/")

  assert skill.name == "my-skill"
  assert skill.description == "Test description"
  assert skill.instructions == "Test instructions"
  # Using dict access for reference
  assert skill.resources.get_reference("ref1.md") == "ref1 content"


def test_list_skills_in_dir(tmp_path):
  """Tests listing skills in a directory."""
  skills_dir = tmp_path / "skills"
  skills_dir.mkdir()

  # Valid skill 1
  skill1_dir = skills_dir / "skill1"
  skill1_dir.mkdir()
  (skill1_dir / "SKILL.md").write_text(
      "---\nname: skill1\ndescription: desc1\n---\nbody"
  )

  # Valid skill 2
  skill2_dir = skills_dir / "skill2"
  skill2_dir.mkdir()
  (skill2_dir / "SKILL.md").write_text(
      "---\nname: skill2\ndescription: desc2\n---\nbody"
  )

  # Invalid skill: missing SKILL.md
  (skills_dir / "invalid-no-md").mkdir()

  # Invalid skill: invalid YAML
  invalid_yaml_dir = skills_dir / "invalid-yaml"
  invalid_yaml_dir.mkdir()
  (invalid_yaml_dir / "SKILL.md").write_text("---\ninvalid: yaml: :\n---\nbody")

  # Invalid skill: name mismatch
  mismatch_dir = skills_dir / "mismatch"
  mismatch_dir.mkdir()
  (mismatch_dir / "SKILL.md").write_text(
      "---\nname: other-name\ndescription: desc\n---\nbody"
  )

  skills = list_skills_in_dir(skills_dir)

  assert len(skills) == 2
  assert "skill1" in skills
  assert "skill2" in skills
  assert skills["skill1"].name == "skill1"
  assert skills["skill2"].name == "skill2"


def test_list_skills_in_dir_missing_base_path(tmp_path):
  """Tests list_skills_in_dir with missing base directory."""

  skills = list_skills_in_dir(tmp_path / "nonexistent")
  assert skills == {}


def test__load_skill_from_zip_bytes():
  """Tests loading a skill directly from in-memory zip file bytes."""

  zip_buffer = io.BytesIO()
  with zipfile.ZipFile(zip_buffer, "w") as z:
    z.writestr(
        "SKILL.md",
        "---\nname: my-skill\ndescription: A skill\n---\nBody instructions",
    )
    z.writestr("references/ref1.md", "ref1 content")
    z.writestr("scripts/script1.sh", "echo hello")

  skill = _load_skill_from_zip_bytes(zip_buffer.getvalue())
  assert skill.frontmatter.name == "my-skill"
  assert skill.frontmatter.description == "A skill"
  assert skill.instructions == "Body instructions"
  assert skill.resources.get_reference("ref1.md") == "ref1 content"
  assert skill.resources.get_script("script1.sh").src == "echo hello"


def test__list_skills_in_gcs_dir_import_error():
  """Tests list_skills_in_gcs_dir raises ImportError when storage missing."""
  real_import = builtins.__import__

  def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
    if name == "google.cloud" and "storage" in (fromlist or ()):
      raise ImportError("No module named 'google.cloud.storage'")
    return real_import(name, globals, locals, fromlist, level)

  with mock.patch("builtins.__import__", mock_import):
    with pytest.raises(ImportError, match="google-cloud-storage is required"):
      _list_skills_in_gcs_dir("my-bucket", "skills/")


def test__load_skill_from_gcs_dir_import_error():
  """Tests load_skill_from_gcs_dir raises ImportError when storage missing."""
  real_import = builtins.__import__

  def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
    if name == "google.cloud" and "storage" in (fromlist or ()):
      raise ImportError("No module named 'google.cloud.storage'")
    return real_import(name, globals, locals, fromlist, level)

  with mock.patch("builtins.__import__", mock_import):
    with pytest.raises(ImportError, match="google-cloud-storage is required"):
      _load_skill_from_gcs_dir("my-bucket", "skills/my-skill/")
