"""Tests for .vrignore file parsing and pattern matching."""

import os
import zipfile
import io
import tempfile
import pytest

from pathlib import Path
from unittest.mock import patch, Mock

from typer.testing import CliRunner

from vr_cli.cli import app
from vr_cli.entities.agent_template import AgentTemplate
from vr_cli.utils.vrignore import load_vrignore_patterns, matches_vrignore


runner = CliRunner()


# ============================================================================
# load_vrignore_patterns
# ============================================================================


class TestLoadVrignorePatterns:
    """Tests for loading .vrignore files."""

    def test_returns_empty_when_no_file(self, temp_dir):
        assert load_vrignore_patterns(temp_dir) == []

    def test_loads_patterns_from_file(self, temp_dir):
        (temp_dir / ".vrignore").write_text("*.log\ndata/\nsecret.txt\n")
        patterns = load_vrignore_patterns(temp_dir)
        assert patterns == ["*.log", "data/", "secret.txt"]

    def test_skips_blank_lines(self, temp_dir):
        (temp_dir / ".vrignore").write_text("*.log\n\n\nsecret.txt\n")
        patterns = load_vrignore_patterns(temp_dir)
        assert patterns == ["*.log", "secret.txt"]

    def test_skips_comments(self, temp_dir):
        (temp_dir / ".vrignore").write_text("# This is a comment\n*.log\n# Another comment\nsecret.txt\n")
        patterns = load_vrignore_patterns(temp_dir)
        assert patterns == ["*.log", "secret.txt"]

    def test_strips_whitespace(self, temp_dir):
        (temp_dir / ".vrignore").write_text("  *.log  \n  secret.txt  \n")
        patterns = load_vrignore_patterns(temp_dir)
        assert patterns == ["*.log", "secret.txt"]

    def test_handles_inline_comment_hash_as_pattern(self, temp_dir):
        """A # not at the start of a line is part of the pattern (gitignore behavior)."""
        (temp_dir / ".vrignore").write_text("file#name\n")
        patterns = load_vrignore_patterns(temp_dir)
        assert patterns == ["file#name"]

    def test_accepts_path_string(self, temp_dir):
        (temp_dir / ".vrignore").write_text("*.log\n")
        patterns = load_vrignore_patterns(str(temp_dir))
        assert patterns == ["*.log"]


# ============================================================================
# matches_vrignore — glob patterns
# ============================================================================


class TestMatchesVrignoreGlobs:
    """Tests for glob-based pattern matching."""

    def test_star_extension_matches_file(self):
        assert matches_vrignore("app.log", ["*.log"]) is True

    def test_star_extension_matches_nested_file(self):
        assert matches_vrignore("sub/dir/app.log", ["*.log"]) is True

    def test_star_extension_no_match(self):
        assert matches_vrignore("app.log.bak", ["*.log"]) is False

    def test_exact_filename_matches(self):
        assert matches_vrignore("secret.txt", ["secret.txt"]) is True

    def test_exact_filename_matches_nested(self):
        assert matches_vrignore("sub/secret.txt", ["secret.txt"]) is True

    def test_question_mark_wildcard(self):
        assert matches_vrignore("file1.txt", ["file?.txt"]) is True
        assert matches_vrignore("file12.txt", ["file?.txt"]) is False

    def test_no_patterns_no_match(self):
        assert matches_vrignore("anything.txt", []) is False


# ============================================================================
# matches_vrignore — directory patterns (trailing /)
# ============================================================================


class TestMatchesVrignoreDirPatterns:
    """Tests for directory-only patterns (trailing /)."""

    def test_dir_pattern_matches_directory(self):
        assert matches_vrignore("data", ["data/"], is_dir=True) is True

    def test_dir_pattern_ignores_file(self):
        assert matches_vrignore("data", ["data/"], is_dir=False) is False

    def test_dir_pattern_matches_nested_directory(self):
        assert matches_vrignore("src/data", ["data/"], is_dir=True) is True


# ============================================================================
# matches_vrignore — anchored patterns (leading /)
# ============================================================================


class TestMatchesVrignoreAnchored:
    """Tests for anchored patterns (leading /)."""

    def test_anchored_matches_at_root(self):
        assert matches_vrignore("config.yaml", ["/config.yaml"]) is True

    def test_anchored_does_not_match_nested(self):
        assert matches_vrignore("sub/config.yaml", ["/config.yaml"]) is False

    def test_anchored_dir_pattern(self):
        assert matches_vrignore("build", ["/build/"], is_dir=True) is True
        assert matches_vrignore("sub/build", ["/build/"], is_dir=True) is False


# ============================================================================
# matches_vrignore — globstar (**)
# ============================================================================


class TestMatchesVrignoreGlobstar:
    """Tests for ** (globstar) patterns."""

    def test_globstar_suffix(self):
        """build/** matches everything under build/."""
        assert matches_vrignore("build/output/file.js", ["build/**"]) is True
        assert matches_vrignore("build/file.js", ["build/**"]) is True

    def test_globstar_prefix(self):
        """**/test.txt matches test.txt in any directory."""
        assert matches_vrignore("test.txt", ["**/test.txt"]) is True
        assert matches_vrignore("a/b/test.txt", ["**/test.txt"]) is True

    def test_globstar_middle(self):
        """src/**/test.py matches test.py under src/ at any depth."""
        assert matches_vrignore("src/test.py", ["src/**/test.py"]) is True
        assert matches_vrignore("src/a/b/test.py", ["src/**/test.py"]) is True

    def test_globstar_no_match(self):
        assert matches_vrignore("other/file.js", ["build/**"]) is False


# ============================================================================
# matches_vrignore — multiple patterns
# ============================================================================


class TestMatchesVrignoreMultiple:
    """Tests for matching against multiple patterns."""

    def test_matches_any_pattern(self):
        patterns = ["*.log", "*.tmp", "secret.txt"]
        assert matches_vrignore("app.log", patterns) is True
        assert matches_vrignore("cache.tmp", patterns) is True
        assert matches_vrignore("secret.txt", patterns) is True
        assert matches_vrignore("handler.py", patterns) is False


# ============================================================================
# Integration: package_function respects .vrignore
# ============================================================================


class TestPackageFunctionVrignore:
    """Tests that package_function excludes files listed in .vrignore."""

    def test_vrignore_excludes_files_from_zip(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            project = Path(tmpdir)

            # Create handler.py (required for packaging)
            (project / "handler.py").write_text("async def handler(): pass")
            # Create files — some should be excluded
            (project / "keep.py").write_text("keep")
            (project / "remove.log").write_text("log data")
            data_dir = project / "data"
            data_dir.mkdir()
            (data_dir / "big.csv").write_text("csv data")

            # Write .vrignore
            (project / ".vrignore").write_text("*.log\ndata/\n")

            from vr_cli.utils.utils import package_function
            zip_bytes, _ = package_function(str(project))

            # Inspect the zip contents
            with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
                names = zf.namelist()

            assert "handler.py" in names
            assert "keep.py" in names
            assert "remove.log" not in names
            assert "data/big.csv" not in names
            # .vrignore itself should be included (it's not in EXCLUDE_PATTERNS)
            assert ".vrignore" in names

    def test_no_vrignore_packages_everything(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            project = Path(tmpdir)
            (project / "handler.py").write_text("async def handler(): pass")
            (project / "extra.log").write_text("log data")

            from vr_cli.utils.utils import package_function
            zip_bytes, _ = package_function(str(project))

            with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
                names = zf.namelist()

            assert "handler.py" in names
            assert "extra.log" in names


# ============================================================================
# Integration: calculate_project_checksum respects .vrignore
# ============================================================================


class TestChecksumVrignore:
    """Tests that calculate_project_checksum respects .vrignore."""

    def test_checksum_changes_when_vrignore_excludes_file(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            project = Path(tmpdir)
            (project / "handler.py").write_text("async def handler(): pass")
            (project / "data.log").write_text("log data")

            from vr_cli.utils.utils import calculate_project_checksum

            # Checksum without .vrignore
            checksum_with_log = calculate_project_checksum(str(project))

            # Add .vrignore that excludes the log file
            (project / ".vrignore").write_text("*.log\n")
            checksum_without_log = calculate_project_checksum(str(project))

            # They should differ because data.log is now excluded
            # (but .vrignore file itself is added, so checksums definitely differ)
            assert checksum_with_log != checksum_without_log


# ============================================================================
# Integration: create template respects .vrignore
# ============================================================================


class TestCreateTemplateVrignore:
    """Tests that vr create template excludes files listed in .vrignore."""

    def test_vrignore_excludes_files_from_template(self, temp_dir, monkeypatch):
        """Files matching .vrignore patterns should not be sent to the API."""
        monkeypatch.chdir(temp_dir)

        # Create project files
        (temp_dir / "handler.py").write_text("async def handler(): pass")
        (temp_dir / "utils.py").write_text("def helper(): pass")
        (temp_dir / "debug.log").write_text("log output")
        data_dir = temp_dir / "data"
        data_dir.mkdir()
        (data_dir / "big.csv").write_text("csv data")

        # Write .vrignore
        (temp_dir / ".vrignore").write_text("*.log\ndata/\n")

        created_template = AgentTemplate(
            id="tmpl-123",
            name="test-template",
            files={"handler.py": "async def handler(): pass"},
        )

        with patch("vr_cli.commands.create.AgentTemplateRepository") as mock_repo_class:
            mock_repo = Mock()
            mock_repo.create.return_value = created_template
            mock_repo_class.return_value = mock_repo

            result = runner.invoke(app, ["create", "template", "test-template"])

        assert result.exit_code == 0

        # Inspect what files were passed to repo.create
        call_args = mock_repo.create.call_args[0][0]
        files = call_args.files

        assert "handler.py" in files
        assert "utils.py" in files
        assert ".vrignore" in files
        assert "debug.log" not in files
        assert "data/big.csv" not in files

    def test_no_vrignore_includes_all_files(self, temp_dir, monkeypatch):
        """Without .vrignore, all text files should be included."""
        monkeypatch.chdir(temp_dir)

        (temp_dir / "handler.py").write_text("async def handler(): pass")
        (temp_dir / "notes.log").write_text("log output")

        created_template = AgentTemplate(
            id="tmpl-456",
            name="test-template",
            files={"handler.py": "async def handler(): pass"},
        )

        with patch("vr_cli.commands.create.AgentTemplateRepository") as mock_repo_class:
            mock_repo = Mock()
            mock_repo.create.return_value = created_template
            mock_repo_class.return_value = mock_repo

            result = runner.invoke(app, ["create", "template", "test-template"])

        assert result.exit_code == 0

        call_args = mock_repo.create.call_args[0][0]
        files = call_args.files

        assert "handler.py" in files
        assert "notes.log" in files

    def test_vrignore_anchored_pattern_in_template(self, temp_dir, monkeypatch):
        """Anchored patterns should only exclude at root level."""
        monkeypatch.chdir(temp_dir)

        (temp_dir / "handler.py").write_text("async def handler(): pass")
        (temp_dir / "config.local.yaml").write_text("local config")
        sub_dir = temp_dir / "sub"
        sub_dir.mkdir()
        (sub_dir / "config.local.yaml").write_text("nested config")

        # Anchored pattern — only root level
        (temp_dir / ".vrignore").write_text("/config.local.yaml\n")

        created_template = AgentTemplate(
            id="tmpl-789",
            name="test-template",
            files={},
        )

        with patch("vr_cli.commands.create.AgentTemplateRepository") as mock_repo_class:
            mock_repo = Mock()
            mock_repo.create.return_value = created_template
            mock_repo_class.return_value = mock_repo

            result = runner.invoke(app, ["create", "template", "test-template"])

        assert result.exit_code == 0

        call_args = mock_repo.create.call_args[0][0]
        files = call_args.files

        assert "config.local.yaml" not in files
        assert "sub/config.local.yaml" in files

    def test_vrignore_all_files_excluded_shows_error(self, temp_dir, monkeypatch):
        """If .vrignore excludes everything, should show error."""
        monkeypatch.chdir(temp_dir)

        (temp_dir / "only.txt").write_text("content")
        (temp_dir / ".vrignore").write_text("*.txt\n.vrignore\n")

        result = runner.invoke(app, ["create", "template", "test-template"])

        assert result.exit_code != 0
        assert "No files found" in result.output
