"""Extended tests for git_tools module."""

import importlib
import subprocess
from pathlib import Path

import pytest

git_tools = importlib.import_module("gjalla_precommit.tools.git_tools")


def _run_git(repo_path: Path, *args: str) -> str:
    """Run git command in repo."""
    result = subprocess.run(
        ["git", *args],
        cwd=repo_path,
        check=True,
        capture_output=True,
        text=True,
    )
    return result.stdout.strip()


class TestNormalizePath:
    """Tests for normalize_path function."""

    def test_normalize_forward_slashes(self):
        """Should handle paths with forward slashes.

        NOTE: PurePosixPath does NOT convert backslashes on Unix systems.
        Backslashes are treated as literal characters, not path separators.
        This behavior is correct for POSIX - on Unix, backslashes are valid
        filename characters. The function primarily ensures forward-slash output.
        """
        # This test verifies forward slash handling works
        result = git_tools.normalize_path("src/main/app.py")
        assert result == "src/main/app.py"

    def test_normalize_already_posix(self):
        """Should preserve already POSIX paths."""
        result = git_tools.normalize_path("src/main/app.py")
        assert result == "src/main/app.py"

    def test_normalize_path_object(self):
        """Should handle Path objects."""
        result = git_tools.normalize_path(Path("src/main/app.py"))
        assert result == "src/main/app.py"


class TestGetDirtyFiles:
    """Tests for get_dirty_files function."""

    def test_dirty_files_empty(self, temp_repo):
        """Should return empty list when no dirty files."""
        result = git_tools.get_dirty_files(temp_repo)
        assert result == []

    def test_dirty_files_modified(self, temp_repo):
        """Should return modified files."""
        # Modify an existing file
        (temp_repo / "README.md").write_text("# Modified\n")
        result = git_tools.get_dirty_files(temp_repo)
        assert "README.md" in result

    def test_dirty_files_staged(self, staged_repo):
        """Should return staged files."""
        result = git_tools.get_dirty_files(staged_repo)
        assert any("login.py" in f for f in result)

    def test_dirty_files_not_git_repo(self, tmp_path):
        """Should return error for non-repo."""
        result = git_tools.get_dirty_files(tmp_path)
        assert len(result) == 1
        assert "Error" in result[0]


class TestLoadIgnorePatterns:
    """Tests for _load_ignore_patterns function."""

    def test_ignore_patterns_gitignore(self, temp_repo):
        """Should load patterns from .gitignore."""
        (temp_repo / ".gitignore").write_text("*.pyc\n__pycache__/\n")
        spec = git_tools._load_ignore_patterns(temp_repo)
        assert spec is not None
        assert spec.match_file("test.pyc")
        assert spec.match_file("__pycache__/file.py")

    def test_ignore_patterns_gjallaignore(self, temp_repo):
        """Should load patterns from .gjallaignore."""
        (temp_repo / ".gjallaignore").write_text("*.log\nsecrets/\n")
        spec = git_tools._load_ignore_patterns(temp_repo)
        assert spec is not None
        assert spec.match_file("app.log")

    def test_ignore_patterns_combined(self, temp_repo):
        """Should combine patterns from both files."""
        (temp_repo / ".gitignore").write_text("*.pyc\n")
        (temp_repo / ".gjallaignore").write_text("*.log\n")
        spec = git_tools._load_ignore_patterns(temp_repo)
        assert spec is not None
        assert spec.match_file("test.pyc")
        assert spec.match_file("app.log")

    def test_ignore_patterns_none(self, temp_repo):
        """Should return None when no ignore files."""
        # Remove any ignore files
        gitignore = temp_repo / ".gitignore"
        if gitignore.exists():
            gitignore.unlink()
        spec = git_tools._load_ignore_patterns(temp_repo)
        # Could be None or empty spec
        if spec is not None:
            # Some files may still not match
            assert not spec.match_file("normal_file.py")

    def test_ignore_patterns_comments_excluded(self, temp_repo):
        """Should ignore comment lines."""
        (temp_repo / ".gitignore").write_text("# This is a comment\n*.pyc\n")
        spec = git_tools._load_ignore_patterns(temp_repo)
        assert spec is not None
        # Comment should not be treated as pattern
        assert not spec.match_file("# This is a comment")


class TestIsIgnored:
    """Tests for is_ignored function."""

    def test_is_ignored_true(self, temp_repo):
        """Should return True for ignored files."""
        (temp_repo / ".gitignore").write_text("*.pyc\n")
        assert git_tools.is_ignored(temp_repo, "test.pyc") is True

    def test_is_ignored_false(self, temp_repo):
        """Should return False for non-ignored files."""
        (temp_repo / ".gitignore").write_text("*.pyc\n")
        assert git_tools.is_ignored(temp_repo, "test.py") is False


class TestGetNonIgnoredFiles:
    """Tests for get_non_ignored_files function."""

    def test_filters_ignored_files(self, temp_repo):
        """Should filter out ignored files."""
        (temp_repo / ".gitignore").write_text("*.pyc\n")
        files = ["main.py", "main.pyc", "utils.py", "cache.pyc"]
        result = git_tools.get_non_ignored_files(temp_repo, files)
        assert "main.py" in result
        assert "utils.py" in result
        assert "main.pyc" not in result
        assert "cache.pyc" not in result


class TestClearCaches:
    """Tests for clear_caches function."""

    def test_clear_caches(self, temp_repo):
        """Should clear internal caches."""
        # Populate caches by calling functions
        git_tools.get_tracked_files(temp_repo, use_cache=True)
        git_tools._load_ignore_patterns(temp_repo)

        # Clear caches
        git_tools.clear_caches()

        # Caches should be empty
        assert len(git_tools._tree_files_cache) == 0
        assert len(git_tools._ignore_spec_cache) == 0


class TestGitStatus:
    """Tests for git_status async function."""

    @pytest.mark.asyncio
    async def test_git_status_success(self, temp_repo):
        """Should return git status output."""
        result = await git_tools.git_status(temp_repo)
        assert result.success is True
        assert "branch" in result.data.lower() or "clean" in result.data.lower()

    @pytest.mark.asyncio
    async def test_git_status_not_repo(self, tmp_path):
        """Should return error for non-repo."""
        result = await git_tools.git_status(tmp_path)
        assert result.success is False
        assert "Not a git repository" in result.error


class TestGitLog:
    """Tests for git_log async function."""

    @pytest.mark.asyncio
    async def test_git_log_success(self, temp_repo):
        """Should return recent commits."""
        result = await git_tools.git_log(temp_repo, n=5)
        assert result.success is True
        # Should have at least the initial commit
        assert len(result.data) > 0

    @pytest.mark.asyncio
    async def test_git_log_not_repo(self, tmp_path):
        """Should return error for non-repo."""
        result = await git_tools.git_log(tmp_path)
        assert result.success is False


class TestGetStagedFiles:
    """Tests for get_staged_files function."""

    def test_staged_files_empty(self, temp_repo):
        """Should return empty list when nothing staged."""
        result = git_tools.get_staged_files(temp_repo)
        assert result == [] or (len(result) == 1 and "Error" in result[0])

    def test_staged_files_with_staged(self, staged_repo):
        """Should return staged files."""
        result = git_tools.get_staged_files(staged_repo)
        assert any("login.py" in f for f in result)

    def test_staged_files_not_repo(self, tmp_path):
        """Should return error for non-repo."""
        result = git_tools.get_staged_files(tmp_path)
        assert len(result) == 1
        assert "Error" in result[0]


class TestGetWorkingDiff:
    """Tests for get_working_diff function."""

    def test_working_diff_clean(self, temp_repo):
        """Should return empty string when no changes."""
        result = git_tools.get_working_diff(temp_repo)
        assert result == ""

    def test_working_diff_modified(self, temp_repo):
        """Should return diff for modified files."""
        (temp_repo / "README.md").write_text("# Modified content\n")
        result = git_tools.get_working_diff(temp_repo)
        assert "Modified content" in result or "diff" in result.lower()

    def test_working_diff_not_repo(self, tmp_path):
        """Should raise error for non-repo."""
        with pytest.raises(git_tools.GitToolError, match="Not a git repository"):
            git_tools.get_working_diff(tmp_path)


class TestGetAllChangesDiff:
    """Tests for get_all_changes_diff function."""

    def test_all_changes_diff_staged_and_unstaged(self, staged_repo):
        """Should include both staged and unstaged changes."""
        # Add an unstaged change
        (staged_repo / "README.md").write_text("# Also modified\n")
        result = git_tools.get_all_changes_diff(staged_repo)
        # Should have changes from both
        assert "diff" in result.lower() or len(result) > 0


class TestGetTrackedFiles:
    """Tests for get_tracked_files function."""

    def test_tracked_files_returns_list(self, temp_repo):
        """Should return list of tracked files."""
        result = git_tools.get_tracked_files(temp_repo)
        assert isinstance(result, list)
        assert "README.md" in result

    def test_tracked_files_uses_cache(self, temp_repo):
        """Should use cache on second call."""
        # First call populates cache
        git_tools.clear_caches()
        result1 = git_tools.get_tracked_files(temp_repo, use_cache=True)
        result2 = git_tools.get_tracked_files(temp_repo, use_cache=True)
        assert result1 == result2

    def test_tracked_files_bypass_cache(self, temp_repo):
        """Should bypass cache when requested."""
        git_tools.clear_caches()
        result = git_tools.get_tracked_files(temp_repo, use_cache=False)
        assert isinstance(result, list)


class TestGetHeadCommit:
    """Tests for get_head_commit function."""

    def test_head_commit_returns_sha(self, temp_repo):
        """Should return commit SHA."""
        result = git_tools.get_head_commit(temp_repo)
        assert len(result) == 40  # Full SHA
        assert all(c in "0123456789abcdef" for c in result)

    def test_head_commit_not_repo(self, tmp_path):
        """Should return error for non-repo."""
        result = git_tools.get_head_commit(tmp_path)
        assert "Error" in result


class TestGetRemoteUrl:
    """Tests for get_remote_url function."""

    def test_remote_url_no_remote(self, temp_repo):
        """Should return None when no origin remote."""
        result = git_tools.get_remote_url(temp_repo)
        assert result is None

    def test_remote_url_with_origin(self, temp_repo):
        """Should return origin URL when present."""
        _run_git(temp_repo, "remote", "add", "origin", "https://github.com/test/repo.git")
        result = git_tools.get_remote_url(temp_repo)
        assert result == "https://github.com/test/repo.git"


class TestExtractTicketReferences:
    """Tests for extract_ticket_references function."""

    def test_extract_jira_style(self):
        """Should extract JIRA-style tickets."""
        refs = git_tools.extract_ticket_references("Fix AUTH-123 bug")
        assert "AUTH-123" in refs

    def test_extract_github_style(self):
        """Should extract GitHub-style tickets."""
        refs = git_tools.extract_ticket_references("Closes #456")
        assert "#456" in refs

    def test_extract_multiple(self):
        """Should extract multiple references."""
        refs = git_tools.extract_ticket_references("Fix AUTH-123 and #789")
        assert "AUTH-123" in refs
        assert "#789" in refs

    def test_extract_none(self):
        """Should return empty list when no references."""
        refs = git_tools.extract_ticket_references("Simple commit message")
        assert refs == []


class TestGetDiff:
    """Tests for get_diff async function."""

    @pytest.mark.asyncio
    async def test_get_diff_between_commits(self, temp_repo):
        """Should return diff between commits."""
        # Create a second commit
        (temp_repo / "new_file.txt").write_text("New content\n")
        _run_git(temp_repo, "add", "new_file.txt")
        _run_git(temp_repo, "commit", "-m", "Add new file")

        # Get diff between HEAD~1 and HEAD
        result = await git_tools.get_diff(temp_repo, "HEAD~1", "HEAD")
        assert result.success is True
        assert "new_file.txt" in result.data or "New content" in result.data


class TestFormatReceiptTrailer:
    """Tests for format_receipt_trailer function."""

    def test_format_trailer(self):
        """Should format receipt ID as trailer."""
        result = git_tools.format_receipt_trailer("gj:abc123")
        assert result == "Gjalla-Receipt: gj:abc123"


class TestAppendReceiptTrailer:
    """Tests for append_receipt_trailer function."""

    def test_append_trailer(self):
        """Should append trailer to message."""
        message = "Fix bug"
        result = git_tools.append_receipt_trailer(message, "gj:abc123")
        assert "Gjalla-Receipt: gj:abc123" in result
        assert result.startswith("Fix bug")

    def test_append_no_duplicate(self):
        """Should not duplicate existing trailer."""
        message = "Fix bug\n\nGjalla-Receipt: gj:abc123\n"
        result = git_tools.append_receipt_trailer(message, "gj:abc123")
        assert result.count("Gjalla-Receipt") == 1


class TestGetCommitHistory:
    """Tests for get_commit_history function."""

    def test_commit_history_returns_list(self, temp_repo):
        """Should return list of commit dicts."""
        result = git_tools.get_commit_history(temp_repo, count=5)
        assert isinstance(result, list)
        assert len(result) >= 1

    def test_commit_history_has_fields(self, temp_repo):
        """Commits should have required fields."""
        result = git_tools.get_commit_history(temp_repo, count=1)
        commit = result[0]
        assert "sha" in commit
        assert "message" in commit
        assert "author" in commit
        assert "date" in commit

    def test_commit_history_not_repo(self, tmp_path):
        """Should return error dict for non-repo."""
        result = git_tools.get_commit_history(tmp_path)
        assert len(result) == 1
        assert "error" in result[0]
