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

"""Tests for upgrade utilities."""

import pathlib
import tempfile
from unittest.mock import MagicMock, patch

import pytest

from agent_starter_pack.cli.utils.upgrade import (
    FileCompareResult,
    categorize_file,
    collect_all_files,
    group_results_by_action,
    merge_pyproject_dependencies,
    three_way_compare,
    update_asp_metadata,
    write_merged_dependencies,
)


class TestCategorizeFile:
    """Tests for file categorization."""

    def test_agent_code_patterns(self) -> None:
        """Test that agent code files are correctly categorized."""
        assert categorize_file("app/agent.py") == "agent_code"
        assert categorize_file("app/tools/search.py") == "agent_code"
        assert categorize_file("app/prompts/main.py") == "agent_code"

    def test_go_agent_code_patterns(self) -> None:
        """Test that Go agent code files are correctly categorized."""
        assert categorize_file("agent/agent.go", "agent") == "agent_code"
        assert categorize_file("agent/tools/search.go", "agent") == "agent_code"
        assert categorize_file("agent/handlers/main.go", "agent") == "agent_code"
        # Default app directory
        assert categorize_file("app/agent.go") == "agent_code"
        assert categorize_file("app/utils/helper.go") == "agent_code"

    def test_java_agent_code_patterns(self) -> None:
        """Test that Java agent code files are correctly categorized."""
        assert (
            categorize_file("src/main/java/myagent/RootAgent.java", "src/main/java")
            == "agent_code"
        )
        assert (
            categorize_file("src/main/java/myagent/tools/Search.java", "src/main/java")
            == "agent_code"
        )
        assert (
            categorize_file("src/main/java/myagent/Main.java", "src/main/java")
            == "agent_code"
        )
        # Default app directory
        assert categorize_file("app/Main.java") == "agent_code"
        assert categorize_file("app/utils/Helper.java") == "agent_code"

    def test_config_files(self) -> None:
        """Test that config files are correctly categorized."""
        assert categorize_file("deployment/vars/dev.tfvars") == "config_files"
        assert categorize_file(".env") == "config_files"

    def test_dependencies(self) -> None:
        """Test that pyproject.toml is categorized as dependencies."""
        assert categorize_file("pyproject.toml") == "dependencies"

    def test_go_dependencies(self) -> None:
        """Test that Go dependency files are correctly categorized."""
        assert categorize_file("go.mod") == "dependencies"
        assert categorize_file("go.sum") == "dependencies"
        assert categorize_file(".asp.toml") == "dependencies"

    def test_java_dependencies(self) -> None:
        """Test that Java dependency files are correctly categorized."""
        assert categorize_file("pom.xml") == "dependencies"
        assert categorize_file(".asp.toml") == "dependencies"

    def test_scaffolding_files(self) -> None:
        """Test that scaffolding files are correctly categorized."""
        assert categorize_file("deployment/terraform/main.tf") == "scaffolding"
        assert categorize_file(".github/workflows/deploy.yaml") == "scaffolding"
        assert categorize_file("Makefile") == "scaffolding"
        assert categorize_file("tests/conftest.py") == "scaffolding"

    def test_custom_agent_directory(self) -> None:
        """Test categorization with custom agent directory."""
        assert categorize_file("my_agent/agent.py", "my_agent") == "agent_code"
        assert categorize_file("my_agent/tools/custom.py", "my_agent") == "agent_code"
        # Default app directory should not match
        assert categorize_file("app/agent.py", "my_agent") == "scaffolding"

    def test_custom_agent_directory_go(self) -> None:
        """Test categorization with custom agent directory for Go."""
        assert categorize_file("my_agent/agent.go", "my_agent") == "agent_code"
        assert categorize_file("my_agent/handlers/api.go", "my_agent") == "agent_code"
        # Default agent directory should not match
        assert categorize_file("agent/agent.go", "my_agent") == "scaffolding"

    def test_custom_agent_directory_java(self) -> None:
        """Test categorization with custom agent directory for Java."""
        assert (
            categorize_file("src/main/java/RootAgent.java", "src/main/java")
            == "agent_code"
        )
        assert (
            categorize_file("src/main/java/myagent/tools/Search.java", "src/main/java")
            == "agent_code"
        )
        # Default app directory should not match when using custom
        assert categorize_file("app/Main.java", "src/main/java") == "scaffolding"


class TestThreeWayCompare:
    """Tests for 3-way file comparison."""

    @pytest.fixture
    def temp_dirs(self) -> tuple[pathlib.Path, pathlib.Path, pathlib.Path]:
        """Create temporary directories for testing."""
        with tempfile.TemporaryDirectory() as temp:
            project = pathlib.Path(temp) / "project"
            old_template = pathlib.Path(temp) / "old"
            new_template = pathlib.Path(temp) / "new"

            project.mkdir()
            old_template.mkdir()
            new_template.mkdir()

            yield project, old_template, new_template

    def test_auto_update_unchanged_by_user(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test auto-update when user didn't modify file."""
        project, old_template, new_template = temp_dirs

        # Create same file in project and old template
        (project / "test.txt").write_text("old content")
        (old_template / "test.txt").write_text("old content")
        # New template has updated content
        (new_template / "test.txt").write_text("new content")

        result = three_way_compare("test.txt", project, old_template, new_template)

        assert result.action == "auto_update"
        assert "didn't modify" in result.reason.lower()

    def test_preserve_asp_unchanged(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test preserve when ASP didn't change file."""
        project, old_template, new_template = temp_dirs

        # User modified file
        (project / "test.txt").write_text("user modified")
        # Old and new template have same content
        (old_template / "test.txt").write_text("template content")
        (new_template / "test.txt").write_text("template content")

        result = three_way_compare("test.txt", project, old_template, new_template)

        assert result.action == "preserve"
        assert "asp didn't change" in result.reason.lower()

    def test_conflict_both_changed(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test conflict when both user and ASP changed file."""
        project, old_template, new_template = temp_dirs

        # All three have different content
        (project / "test.txt").write_text("user content")
        (old_template / "test.txt").write_text("old content")
        (new_template / "test.txt").write_text("new content")

        result = three_way_compare("test.txt", project, old_template, new_template)

        assert result.action == "conflict"
        assert "both" in result.reason.lower()

    def test_new_file_in_asp(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test detection of new file in ASP."""
        project, old_template, new_template = temp_dirs

        # Only exists in new template
        (new_template / "new_file.txt").write_text("new content")

        result = three_way_compare("new_file.txt", project, old_template, new_template)

        assert result.action == "new"
        assert "new file" in result.reason.lower()

    def test_new_file_in_asp_with_old_template_version(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test that a file not in the project is treated as new even if it exists in old template.

        This covers the case of switching deployment targets (e.g. none -> agent_engine)
        where the file exists in both templates with different content but not in the project.
        """
        project, old_template, new_template = temp_dirs

        # File exists in both templates (different content) but NOT in the project
        (old_template / "deploy_file.py").write_text("cloud_run version")
        (new_template / "deploy_file.py").write_text("agent_engine version")

        result = three_way_compare(
            "deploy_file.py", project, old_template, new_template
        )

        assert result.action == "new"
        assert "new file" in result.reason.lower()

    def test_removed_file_in_asp_user_unchanged(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test detection of removed file when user didn't modify."""
        project, old_template, new_template = temp_dirs

        # File exists in project and old template, not in new
        (project / "old_file.txt").write_text("same content")
        (old_template / "old_file.txt").write_text("same content")

        result = three_way_compare("old_file.txt", project, old_template, new_template)

        assert result.action == "removed"

    def test_removed_file_in_asp_user_modified(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test conflict when removed file was modified by user."""
        project, old_template, new_template = temp_dirs

        # User modified a file that was removed in new template
        (project / "old_file.txt").write_text("user modified")
        (old_template / "old_file.txt").write_text("original content")

        result = three_way_compare("old_file.txt", project, old_template, new_template)

        assert result.action == "conflict"

    def test_user_added_file_skipped(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test that user-added files (not in any template) are skipped."""
        project, old_template, new_template = temp_dirs

        # File only exists in the current project, not in either template
        (project / "my_custom_file.txt").write_text("user content")

        result = three_way_compare(
            "my_custom_file.txt", project, old_template, new_template
        )

        assert result.action == "skip"
        assert "user-added" in result.reason.lower()

    def test_skip_agent_code(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test that agent code is always skipped."""
        project, old_template, new_template = temp_dirs

        (project / "app").mkdir()
        (project / "app/agent.py").write_text("user agent")
        (old_template / "app").mkdir()
        (old_template / "app/agent.py").write_text("old agent")
        (new_template / "app").mkdir()
        (new_template / "app/agent.py").write_text("new agent")

        result = three_way_compare("app/agent.py", project, old_template, new_template)

        assert result.action == "skip"
        assert result.category == "agent_code"

    def test_skip_config_files(
        self, temp_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test that config files are always skipped."""
        project, old_template, new_template = temp_dirs

        (project / ".env").write_text("SECRET=user")
        (old_template / ".env").write_text("SECRET=old")
        (new_template / ".env").write_text("SECRET=new")

        result = three_way_compare(".env", project, old_template, new_template)

        assert result.action == "skip"
        assert result.category == "config_files"


class TestCollectAllFiles:
    """Tests for collecting files from directories."""

    def test_collects_from_all_dirs(self) -> None:
        """Test that files are collected from all three directories."""
        with tempfile.TemporaryDirectory() as temp:
            project = pathlib.Path(temp) / "project"
            old_template = pathlib.Path(temp) / "old"
            new_template = pathlib.Path(temp) / "new"

            project.mkdir()
            old_template.mkdir()
            new_template.mkdir()

            (project / "project_file.txt").write_text("content")
            (old_template / "old_file.txt").write_text("content")
            (new_template / "new_file.txt").write_text("content")

            files = collect_all_files(project, old_template, new_template)

            assert "project_file.txt" in files
            assert "old_file.txt" in files
            assert "new_file.txt" in files

    def test_excludes_patterns(self) -> None:
        """Test that excluded patterns are not collected."""
        with tempfile.TemporaryDirectory() as temp:
            project = pathlib.Path(temp) / "project"
            project.mkdir()

            (project / ".git").mkdir()
            (project / ".git/config").write_text("content")
            (project / "real_file.txt").write_text("content")

            files = collect_all_files(
                project, project, project, exclude_patterns=[".git/**"]
            )

            assert ".git/config" not in files
            assert "real_file.txt" in files


class TestGroupResultsByAction:
    """Tests for grouping results by action."""

    def test_groups_correctly(self) -> None:
        """Test that results are grouped by action."""
        results = [
            FileCompareResult("file1.txt", "scaffolding", "auto_update", "reason"),
            FileCompareResult("file2.txt", "scaffolding", "preserve", "reason"),
            FileCompareResult("file3.txt", "scaffolding", "conflict", "reason"),
            FileCompareResult("file4.txt", "scaffolding", "auto_update", "reason"),
        ]

        groups = group_results_by_action(results)

        assert len(groups["auto_update"]) == 2
        assert len(groups["preserve"]) == 1
        assert len(groups["conflict"]) == 1
        assert len(groups["skip"]) == 0


class TestMergePyprojectDependencies:
    """Tests for dependency merging."""

    @pytest.fixture
    def pyproject_dirs(self) -> tuple[pathlib.Path, pathlib.Path, pathlib.Path]:
        """Create temporary directories with pyproject.toml files."""
        with tempfile.TemporaryDirectory() as temp:
            current = pathlib.Path(temp) / "current"
            old_template = pathlib.Path(temp) / "old"
            new_template = pathlib.Path(temp) / "new"

            current.mkdir()
            old_template.mkdir()
            new_template.mkdir()

            yield current, old_template, new_template

    def _write_pyproject(self, path: pathlib.Path, deps: list[str]) -> None:
        """Write a pyproject.toml with the given dependencies."""
        deps_str = ", ".join(f'"{d}"' for d in deps)
        content = f"""
[project]
name = "test-project"
dependencies = [{deps_str}]
"""
        (path / "pyproject.toml").write_text(content)

    def test_updated_dependency(
        self, pyproject_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test detection of updated dependency."""
        current, old_template, new_template = pyproject_dirs

        self._write_pyproject(current, ["google-adk>=0.2.0"])
        self._write_pyproject(old_template, ["google-adk>=0.2.0"])
        self._write_pyproject(new_template, ["google-adk>=0.3.0"])

        result = merge_pyproject_dependencies(
            current / "pyproject.toml",
            old_template / "pyproject.toml",
            new_template / "pyproject.toml",
        )

        updated = [c for c in result.changes if c.change_type == "updated"]
        assert len(updated) == 1
        assert updated[0].name == "google-adk"

    def test_user_added_dependency_kept(
        self, pyproject_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test that user-added dependencies are preserved."""
        current, old_template, new_template = pyproject_dirs

        self._write_pyproject(current, ["google-adk>=0.2.0", "my-custom-lib>=1.0.0"])
        self._write_pyproject(old_template, ["google-adk>=0.2.0"])
        self._write_pyproject(new_template, ["google-adk>=0.3.0"])

        result = merge_pyproject_dependencies(
            current / "pyproject.toml",
            old_template / "pyproject.toml",
            new_template / "pyproject.toml",
        )

        kept = [c for c in result.changes if c.change_type == "kept"]
        assert len(kept) == 1
        assert kept[0].name == "my-custom-lib"

        # Check merged deps contains both
        merged_names = [d.split(">")[0].split("=")[0] for d in result.merged_deps]
        assert "google-adk" in merged_names
        assert "my-custom-lib" in merged_names

    def test_new_asp_dependency(
        self, pyproject_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test detection of new ASP dependency."""
        current, old_template, new_template = pyproject_dirs

        self._write_pyproject(current, ["google-adk>=0.2.0"])
        self._write_pyproject(old_template, ["google-adk>=0.2.0"])
        self._write_pyproject(new_template, ["google-adk>=0.3.0", "new-dep>=1.0.0"])

        result = merge_pyproject_dependencies(
            current / "pyproject.toml",
            old_template / "pyproject.toml",
            new_template / "pyproject.toml",
        )

        added = [c for c in result.changes if c.change_type == "added"]
        assert len(added) == 1
        assert added[0].name == "new-dep"

    def test_removed_asp_dependency(
        self, pyproject_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test detection of removed ASP dependency."""
        current, old_template, new_template = pyproject_dirs

        self._write_pyproject(current, ["google-adk>=0.2.0", "old-dep>=1.0.0"])
        self._write_pyproject(old_template, ["google-adk>=0.2.0", "old-dep>=1.0.0"])
        self._write_pyproject(new_template, ["google-adk>=0.3.0"])

        result = merge_pyproject_dependencies(
            current / "pyproject.toml",
            old_template / "pyproject.toml",
            new_template / "pyproject.toml",
        )

        removed = [c for c in result.changes if c.change_type == "removed"]
        assert len(removed) == 1
        assert removed[0].name == "old-dep"

    def test_extras_change_detected_as_update(
        self, pyproject_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test that changing extras on same package is detected as update, not add+remove."""
        current, old_template, new_template = pyproject_dirs

        self._write_pyproject(
            current,
            ["google-cloud-aiplatform[evaluation]>=1.0.0"],
        )
        self._write_pyproject(
            old_template,
            ["google-cloud-aiplatform[evaluation]>=1.0.0"],
        )
        self._write_pyproject(
            new_template,
            ["google-cloud-aiplatform[evaluation,agent-engines]>=1.1.0"],
        )

        result = merge_pyproject_dependencies(
            current / "pyproject.toml",
            old_template / "pyproject.toml",
            new_template / "pyproject.toml",
        )

        # Should be one "updated" change, not an "added" + "removed"
        updated = [c for c in result.changes if c.change_type == "updated"]
        added = [c for c in result.changes if c.change_type == "added"]
        removed = [c for c in result.changes if c.change_type == "removed"]
        assert len(updated) == 1
        assert updated[0].name == "google-cloud-aiplatform"
        assert len(added) == 0
        assert len(removed) == 0

        # Merged deps should contain the new extras
        assert any(
            "google-cloud-aiplatform[evaluation,agent-engines]" in d
            for d in result.merged_deps
        )

    def test_extras_treated_as_same_package(
        self, pyproject_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test that packages with different extras are treated as the same package."""
        current, old_template, new_template = pyproject_dirs

        self._write_pyproject(current, ["google-cloud-aiplatform[evaluation]>=1.0.0"])
        self._write_pyproject(
            old_template, ["google-cloud-aiplatform[evaluation]>=1.0.0"]
        )
        self._write_pyproject(
            new_template,
            ["google-cloud-aiplatform[evaluation,agent-engines]>=1.1.0"],
        )

        result = merge_pyproject_dependencies(
            current / "pyproject.toml",
            old_template / "pyproject.toml",
            new_template / "pyproject.toml",
        )

        # Should be one "updated" change, not separate add/remove
        updated = [c for c in result.changes if c.change_type == "updated"]
        added = [c for c in result.changes if c.change_type == "added"]
        removed = [c for c in result.changes if c.change_type == "removed"]

        assert len(updated) == 1
        assert updated[0].name == "google-cloud-aiplatform"
        assert len(added) == 0
        assert len(removed) == 0

    def test_extras_preserved_in_merged_deps(
        self, pyproject_dirs: tuple[pathlib.Path, pathlib.Path, pathlib.Path]
    ) -> None:
        """Test that extras brackets are preserved in merged dependency output."""
        current, old_template, new_template = pyproject_dirs

        self._write_pyproject(current, ["google-adk>=0.2.0"])
        self._write_pyproject(old_template, ["google-adk>=0.2.0"])
        self._write_pyproject(new_template, ["google-adk[extra]>=0.3.0"])

        result = merge_pyproject_dependencies(
            current / "pyproject.toml",
            old_template / "pyproject.toml",
            new_template / "pyproject.toml",
        )

        # Merged deps should include the extras
        assert any("google-adk[extra]>=0.3.0" in dep for dep in result.merged_deps)


class TestWriteMergedDependencies:
    """Tests for writing merged dependencies via uv CLI."""

    @patch("agent_starter_pack.cli.utils.upgrade.subprocess.run")
    def test_calls_uv_add_with_merged_deps(
        self, mock_run: MagicMock, tmp_path: pathlib.Path
    ) -> None:
        """Test that uv add --frozen is called with the merged deps."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(
            '[project]\nname = "test"\ndependencies = [\n    "old-dep>=1.0.0",\n]\n'
        )
        mock_run.return_value = MagicMock(returncode=0, stderr="")

        merged_deps = ["google-adk>=0.3.0", "my-custom-lib>=1.0.0"]
        result = write_merged_dependencies(pyproject, merged_deps)

        assert result is True
        # Should call uv remove for old-dep, then uv add for merged deps
        calls = mock_run.call_args_list
        assert len(calls) == 2

        remove_call = calls[0]
        assert remove_call[0][0][:3] == ["uv", "remove", "--frozen"]
        assert "old-dep" in remove_call[0][0]

        add_call = calls[1]
        assert add_call[0][0][:3] == ["uv", "add", "--frozen"]
        assert "google-adk>=0.3.0" in add_call[0][0]
        assert "my-custom-lib>=1.0.0" in add_call[0][0]

    @patch("agent_starter_pack.cli.utils.upgrade.subprocess.run")
    def test_skips_remove_when_no_removals(
        self, mock_run: MagicMock, tmp_path: pathlib.Path
    ) -> None:
        """Test that uv remove is not called when there are no deps to remove."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(
            '[project]\nname = "test"\ndependencies = [\n    "google-adk>=0.2.0",\n]\n'
        )
        mock_run.return_value = MagicMock(returncode=0, stderr="")

        merged_deps = ["google-adk>=0.3.0", "new-dep>=1.0.0"]
        result = write_merged_dependencies(pyproject, merged_deps)

        assert result is True
        # Only uv add should be called (google-adk is in both lists)
        assert len(mock_run.call_args_list) == 1
        add_call = mock_run.call_args_list[0]
        assert add_call[0][0][:3] == ["uv", "add", "--frozen"]

    @patch("agent_starter_pack.cli.utils.upgrade.subprocess.run")
    def test_handles_empty_merged_deps(
        self, mock_run: MagicMock, tmp_path: pathlib.Path
    ) -> None:
        """Test writing empty dependencies list removes all current deps."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(
            '[project]\nname = "test"\ndependencies = [\n    "some-dep>=1.0.0",\n]\n'
        )
        mock_run.return_value = MagicMock(returncode=0, stderr="")

        result = write_merged_dependencies(pyproject, [])

        assert result is True
        # Should call uv remove for some-dep, no uv add
        assert len(mock_run.call_args_list) == 1
        remove_call = mock_run.call_args_list[0]
        assert remove_call[0][0][:3] == ["uv", "remove", "--frozen"]
        assert "some-dep" in remove_call[0][0]

    def test_returns_false_for_missing_file(self, tmp_path: pathlib.Path) -> None:
        """Test that missing file returns False."""
        pyproject = tmp_path / "nonexistent.toml"

        result = write_merged_dependencies(pyproject, ["dep>=1.0.0"])

        assert result is False


class TestCollectAllFilesDeepExclusion:
    """Tests for ** glob pattern exclusions in collect_all_files."""

    def test_excludes_deeply_nested_git_files(self) -> None:
        """Test that .git/** excludes deeply nested files."""
        with tempfile.TemporaryDirectory() as temp:
            project = pathlib.Path(temp) / "project"
            project.mkdir()

            # Create deeply nested .git structure
            git_dir = project / ".git" / "objects" / "pack"
            git_dir.mkdir(parents=True)
            (git_dir / "pack-123.idx").write_text("content")
            (project / ".git" / "config").write_text("content")
            (project / ".git" / "HEAD").write_text("ref: refs/heads/main")

            # Create a real file
            (project / "README.md").write_text("content")

            files = collect_all_files(
                project, project, project, exclude_patterns=[".git/**"]
            )

            assert ".git/config" not in files
            assert ".git/HEAD" not in files
            assert ".git/objects/pack/pack-123.idx" not in files
            assert "README.md" in files

    def test_excludes_venv_deeply_nested(self) -> None:
        """Test that .venv/** excludes deeply nested virtual env files."""
        with tempfile.TemporaryDirectory() as temp:
            project = pathlib.Path(temp) / "project"
            project.mkdir()

            # Create nested venv structure
            venv_lib = project / ".venv" / "lib" / "python3.12" / "site-packages"
            venv_lib.mkdir(parents=True)
            (venv_lib / "some_package" / "__init__.py").parent.mkdir()
            (venv_lib / "some_package" / "__init__.py").write_text("content")

            # Create a real file
            (project / "main.py").write_text("content")

            files = collect_all_files(
                project, project, project, exclude_patterns=[".venv/**"]
            )

            assert (
                ".venv/lib/python3.12/site-packages/some_package/__init__.py"
                not in files
            )
            assert "main.py" in files


class TestUpdateAspMetadata:
    """Tests for updating ASP metadata across all supported languages."""

    def test_python_updates_create_params(self, tmp_path: pathlib.Path) -> None:
        """Test that Python create_params keys are updated correctly."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(
            """
[project]
name = "test-project"

[tool.agent-starter-pack]
name = "test-project"
asp_version = "0.25.0"

[tool.agent-starter-pack.create_params]
deployment_target = "agent_engine"
cicd_runner = "skip"
"""
        )

        result = update_asp_metadata(
            tmp_path,
            {"deployment_target": "cloud_run", "cicd_runner": "github_actions"},
            language="python",
        )

        assert result is True
        content = pyproject.read_text()
        assert 'deployment_target = "cloud_run"' in content
        assert 'cicd_runner = "github_actions"' in content

    def test_python_updates_asp_version(self, tmp_path: pathlib.Path) -> None:
        """Test that asp_version is updated when provided."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(
            """
[tool.agent-starter-pack]
asp_version = "0.25.0"

[tool.agent-starter-pack.create_params]
deployment_target = "agent_engine"
"""
        )

        result = update_asp_metadata(
            tmp_path,
            {"deployment_target": "cloud_run"},
            asp_version="0.30.0",
            language="python",
        )

        assert result is True
        content = pyproject.read_text()
        assert 'asp_version = "0.30.0"' in content
        assert 'deployment_target = "cloud_run"' in content

    def test_go_updates_asp_toml(self, tmp_path: pathlib.Path) -> None:
        """Test that Go .asp.toml metadata is updated correctly."""
        asp_toml = tmp_path / ".asp.toml"
        asp_toml.write_text(
            """
[project]
base_template = "adk_go"
version = "0.25.0"
language = "go"
deployment_target = "agent_engine"
cicd_runner = "skip"
"""
        )

        result = update_asp_metadata(
            tmp_path,
            {"deployment_target": "cloud_run", "cicd_runner": "github_actions"},
            asp_version="0.30.0",
            language="go",
        )

        assert result is True
        content = asp_toml.read_text()
        assert 'deployment_target = "cloud_run"' in content
        assert 'cicd_runner = "github_actions"' in content
        assert 'version = "0.30.0"' in content

    def test_java_updates_pom_xml(self, tmp_path: pathlib.Path) -> None:
        """Test that Java pom.xml ASP properties are updated correctly."""
        pom = tmp_path / "pom.xml"
        pom.write_text(
            """<?xml version="1.0" encoding="UTF-8"?>
<project>
  <properties>
    <asp.version>0.25.0</asp.version>
    <asp.base_template>adk_java</asp.base_template>
    <asp.deployment_target>agent_engine</asp.deployment_target>
    <asp.cicd_runner>skip</asp.cicd_runner>
  </properties>
</project>
"""
        )

        result = update_asp_metadata(
            tmp_path,
            {"deployment_target": "cloud_run", "cicd_runner": "github_actions"},
            asp_version="0.30.0",
            language="java",
        )

        assert result is True
        content = pom.read_text()
        assert "<asp.deployment_target>cloud_run</asp.deployment_target>" in content
        assert "<asp.cicd_runner>github_actions</asp.cicd_runner>" in content
        assert "<asp.version>0.30.0</asp.version>" in content

    def test_typescript_updates_asp_toml(self, tmp_path: pathlib.Path) -> None:
        """Test that TypeScript .asp.toml metadata is updated correctly."""
        asp_toml = tmp_path / ".asp.toml"
        asp_toml.write_text(
            """
[project]
base_template = "adk_ts"
version = "0.25.0"
language = "typescript"
deployment_target = "cloud_run"
"""
        )

        result = update_asp_metadata(
            tmp_path,
            {"deployment_target": "agent_engine"},
            language="typescript",
        )

        assert result is True
        content = asp_toml.read_text()
        assert 'deployment_target = "agent_engine"' in content

    def test_python_removes_stale_keys(self, tmp_path: pathlib.Path) -> None:
        """Test that stale keys like session_type are removed from pyproject.toml."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(
            """[tool.agent-starter-pack]
name = "test-project"

[tool.agent-starter-pack.create_params]
deployment_target = "cloud_run"
session_type = "in_memory"
cicd_runner = "skip"
"""
        )

        result = update_asp_metadata(
            tmp_path,
            {"deployment_target": "agent_engine"},
            language="python",
            remove_keys=["session_type"],
        )

        assert result is True
        content = pyproject.read_text()
        assert 'deployment_target = "agent_engine"' in content
        assert "session_type" not in content
        assert 'cicd_runner = "skip"' in content

    def test_java_removes_stale_keys(self, tmp_path: pathlib.Path) -> None:
        """Test that stale keys are removed from pom.xml."""
        pom = tmp_path / "pom.xml"
        pom.write_text(
            """<?xml version="1.0" encoding="UTF-8"?>
<project>
  <properties>
    <asp.deployment_target>cloud_run</asp.deployment_target>
    <asp.session_type>in_memory</asp.session_type>
    <asp.cicd_runner>skip</asp.cicd_runner>
  </properties>
</project>
"""
        )

        result = update_asp_metadata(
            tmp_path,
            {"deployment_target": "agent_engine"},
            language="java",
            remove_keys=["session_type"],
        )

        assert result is True
        content = pom.read_text()
        assert "<asp.deployment_target>agent_engine</asp.deployment_target>" in content
        assert "session_type" not in content
        assert "<asp.cicd_runner>skip</asp.cicd_runner>" in content

    def test_go_removes_stale_keys(self, tmp_path: pathlib.Path) -> None:
        """Test that stale keys are removed from .asp.toml."""
        asp_toml = tmp_path / ".asp.toml"
        asp_toml.write_text(
            """[project]
deployment_target = "cloud_run"
session_type = "in_memory"
cicd_runner = "skip"
"""
        )

        result = update_asp_metadata(
            tmp_path,
            {"deployment_target": "agent_engine"},
            language="go",
            remove_keys=["session_type"],
        )

        assert result is True
        content = asp_toml.read_text()
        assert 'deployment_target = "agent_engine"' in content
        assert "session_type" not in content
        assert 'cicd_runner = "skip"' in content

    def test_returns_false_for_missing_file(self, tmp_path: pathlib.Path) -> None:
        """Test that missing config file returns False."""
        result = update_asp_metadata(
            tmp_path, {"deployment_target": "cloud_run"}, language="python"
        )

        assert result is False


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
