import pytest
import subprocess
from commit_check.util import (
    fetch_remote_ref,
    fetch_upstream_ref,
    get_branch_name,
    get_git_remotes,
    get_remote_branch_sha,
    get_upstream_branch,
    get_upstream_remote_sha,
    has_commits,
    git_merge_base,
    get_commit_info,
    cmd_output,
    print_error_header,
    print_error_message,
    print_suggestion,
    supports_hyperlinks,
    hyperlink,
    _print_failure,
)
from subprocess import CalledProcessError, PIPE
from unittest.mock import MagicMock

# String constants used across tests
REFS_HEADS_MAIN = "refs/heads/main"
USER_NAME_CONFIG = "user.name"


class TestUtil:
    class TestGetBranchName:
        @pytest.mark.benchmark
        def test_get_branch_name(self, mocker):
            # Must call cmd_output with given argument.
            m_cmd_output = mocker.patch(
                "commit_check.util.cmd_output", return_value=" fake_branch_name "
            )
            retval = get_branch_name()
            assert m_cmd_output.call_count == 1
            assert m_cmd_output.call_args[0][0] == ["git", "branch", "--show-current"]
            assert retval == "fake_branch_name"

        @pytest.mark.benchmark
        def test_get_branch_name_with_exception(self, mocker):
            mock_cmd_output = mocker.patch(
                "commit_check.util.cmd_output",
                side_effect=CalledProcessError(
                    returncode=1, cmd="git branch --show-current"
                ),
            )
            retval = get_branch_name()
            assert mock_cmd_output.call_count == 1
            mock_cmd_output.assert_called_once_with(["git", "branch", "--show-current"])
            assert retval == "HEAD" or retval  # depending on env vars

        @pytest.mark.benchmark
        def test_get_branch_name_fallback_github_head_ref(self, mocker):
            """Test fallback to GITHUB_HEAD_REF."""
            mocker.patch("commit_check.util.cmd_output", return_value="")
            mocker.patch(
                "commit_check.util.os.getenv",
                lambda key: "feature-branch" if key == "GITHUB_HEAD_REF" else None,
            )
            assert get_branch_name() == "feature-branch"

        @pytest.mark.benchmark
        def test_get_branch_name_fallback_github_ref_name(self, mocker):
            """Test fallback to GITHUB_REF_NAME."""
            mocker.patch("commit_check.util.cmd_output", return_value="")
            mocker.patch(
                "commit_check.util.os.getenv",
                lambda key: "develop" if key == "GITHUB_REF_NAME" else None,
            )
            assert get_branch_name() == "develop"

        @pytest.mark.benchmark
        def test_get_branch_name_fallback_head(self, mocker):
            """Test fallback to HEAD."""
            mocker.patch("commit_check.util.cmd_output", return_value="")
            mocker.patch("commit_check.util.os.getenv", return_value=None)
            assert get_branch_name() == "HEAD"

        @pytest.mark.benchmark
        def test_get_branch_name_fallback_priority(self, mocker):
            """Test fallback priority."""
            mocker.patch("commit_check.util.cmd_output", return_value="")
            mocker.patch(
                "commit_check.util.os.getenv",
                lambda key: {
                    "GITHUB_HEAD_REF": "feature-branch",
                    "GITHUB_REF_NAME": "develop",
                }.get(key),
            )
            assert get_branch_name() == "feature-branch"

    class TestHasCommits:
        @pytest.mark.benchmark
        def test_has_commits_true(self, mocker):
            # Must return True when git rev-parse HEAD succeeds
            m_subprocess_run = mocker.patch("subprocess.run", return_value=None)
            retval = has_commits()
            assert m_subprocess_run.call_count == 1
            assert m_subprocess_run.call_args[0][0] == [
                "git",
                "rev-parse",
                "--verify",
                "HEAD",
            ]
            assert m_subprocess_run.call_args[1] == {
                "stdout": subprocess.DEVNULL,
                "stderr": subprocess.DEVNULL,
                "check": True,
            }
            assert retval is True

        @pytest.mark.benchmark
        def test_has_commits_false(self, mocker):
            # Must return False when git rev-parse HEAD fails
            m_subprocess_run = mocker.patch(
                "subprocess.run",
                side_effect=subprocess.CalledProcessError(128, "git rev-parse"),
            )
            retval = has_commits()
            assert m_subprocess_run.call_count == 1
            assert m_subprocess_run.call_args[0][0] == [
                "git",
                "rev-parse",
                "--verify",
                "HEAD",
            ]
            assert m_subprocess_run.call_args[1] == {
                "stdout": subprocess.DEVNULL,
                "stderr": subprocess.DEVNULL,
                "check": True,
            }
            assert retval is False

    class TestGetUpstreamBranch:
        @pytest.mark.benchmark
        def test_get_upstream_branch(self, mocker):
            mock_run = mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {"stdout": "origin/main\n", "stderr": "", "returncode": 0},
                )(),
            )

            result = get_upstream_branch()

            mock_run.assert_called_once_with(
                [
                    "git",
                    "rev-parse",
                    "--abbrev-ref",
                    "--symbolic-full-name",
                    "@{upstream}",
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                encoding="utf-8",
            )
            assert result == "origin/main"

        @pytest.mark.benchmark
        def test_get_upstream_branch_missing(self, mocker):
            mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {
                        "stdout": "",
                        "stderr": "fatal: no upstream",
                        "returncode": 128,
                    },
                )(),
            )

            assert get_upstream_branch() == ""

    class TestGetUpstreamRemoteSha:
        @pytest.mark.benchmark
        def test_get_upstream_remote_sha(self, mocker):
            mock_run = mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {
                        "stdout": "abc123\trefs/heads/main\n",
                        "stderr": "",
                        "returncode": 0,
                    },
                )(),
            )

            result = get_upstream_remote_sha("origin/main")

            mock_run.assert_called_once_with(
                ["git", "ls-remote", "--exit-code", "origin", REFS_HEADS_MAIN],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                encoding="utf-8",
            )
            assert result == "abc123"

        @pytest.mark.benchmark
        def test_get_upstream_remote_sha_with_nested_branch(self, mocker):
            mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {
                        "stdout": "def456\trefs/heads/feature/topic\n",
                        "stderr": "",
                        "returncode": 0,
                    },
                )(),
            )

            assert get_upstream_remote_sha("origin/feature/topic") == "def456"

        @pytest.mark.benchmark
        def test_get_upstream_remote_sha_missing(self, mocker):
            mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {"stdout": "", "stderr": "fatal", "returncode": 2},
                )(),
            )

            assert get_upstream_remote_sha("origin/main") == ""

        @pytest.mark.benchmark
        def test_get_upstream_remote_sha_invalid_ref(self, mocker):
            mock_run = mocker.patch("subprocess.run")

            assert get_upstream_remote_sha("main") == ""
            mock_run.assert_not_called()

    class TestGetRemoteBranchSha:
        @pytest.mark.benchmark
        def test_get_remote_branch_sha(self, mocker):
            mock_run = mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {
                        "stdout": "abc123\trefs/heads/main\n",
                        "stderr": "",
                        "returncode": 0,
                    },
                )(),
            )

            result = get_remote_branch_sha("origin", "main")

            mock_run.assert_called_once_with(
                ["git", "ls-remote", "--exit-code", "origin", REFS_HEADS_MAIN],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                encoding="utf-8",
            )
            assert result == "abc123"

        @pytest.mark.benchmark
        @pytest.mark.parametrize(
            "remote_name,branch_name",
            [
                ("", "main"),
                ("origin", ""),
            ],
        )
        def test_get_remote_branch_sha_invalid_args(
            self, mocker, remote_name, branch_name
        ):
            mock_run = mocker.patch("subprocess.run")

            assert get_remote_branch_sha(remote_name, branch_name) == ""
            mock_run.assert_not_called()

    class TestFetchUpstreamRef:
        @pytest.mark.benchmark
        def test_fetch_upstream_ref(self, mocker):
            mock_run = mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {"stdout": "", "stderr": "", "returncode": 0},
                )(),
            )

            assert fetch_upstream_ref("origin/main") is True
            mock_run.assert_called_once_with(
                ["git", "fetch", "--quiet", "--no-tags", "origin", "main"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                encoding="utf-8",
            )

        @pytest.mark.benchmark
        def test_fetch_upstream_ref_failure(self, mocker):
            mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {"stdout": "", "stderr": "fatal", "returncode": 1},
                )(),
            )

            assert fetch_upstream_ref("origin/main") is False

        @pytest.mark.benchmark
        def test_fetch_upstream_ref_invalid_ref(self, mocker):
            mock_run = mocker.patch("subprocess.run")

            assert fetch_upstream_ref("main") is False
            mock_run.assert_not_called()

    class TestGetGitRemotes:
        @pytest.mark.benchmark
        def test_get_git_remotes(self, mocker):
            mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {"stdout": "origin\nupstream\n", "stderr": "", "returncode": 0},
                )(),
            )

            assert get_git_remotes() == ["origin", "upstream"]

        @pytest.mark.benchmark
        def test_get_git_remotes_failure(self, mocker):
            mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {"stdout": "", "stderr": "fatal", "returncode": 128},
                )(),
            )

            assert get_git_remotes() == []

    class TestFetchRemoteRef:
        @pytest.mark.benchmark
        def test_fetch_remote_ref(self, mocker):
            mock_run = mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {"stdout": "", "stderr": "", "returncode": 0},
                )(),
            )

            assert fetch_remote_ref("origin", REFS_HEADS_MAIN) is True
            mock_run.assert_called_once_with(
                ["git", "fetch", "--quiet", "--no-tags", "origin", REFS_HEADS_MAIN],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                encoding="utf-8",
            )

        @pytest.mark.benchmark
        def test_fetch_remote_ref_failure(self, mocker):
            mocker.patch(
                "subprocess.run",
                return_value=type(
                    "MockResult",
                    (),
                    {"stdout": "", "stderr": "fatal", "returncode": 1},
                )(),
            )

            assert fetch_remote_ref("origin", REFS_HEADS_MAIN) is False

        @pytest.mark.benchmark
        @pytest.mark.parametrize(
            "remote_name,remote_ref",
            [
                ("", REFS_HEADS_MAIN),
                ("origin", ""),
            ],
        )
        def test_fetch_remote_ref_invalid_args(self, mocker, remote_name, remote_ref):
            mock_run = mocker.patch("subprocess.run")

            assert fetch_remote_ref(remote_name, remote_ref) is False
            mock_run.assert_not_called()

    class TestGitMergeBase:
        @pytest.mark.benchmark
        @pytest.mark.parametrize(
            "returncode,expected",
            [
                (0, 0),  # ancestor exists
                (1, 1),  # no ancestor
                (128, 128),  # error case
            ],
        )
        @pytest.mark.benchmark
        def test_git_merge_base(self, mocker, returncode, expected):
            mock_run = mocker.patch("subprocess.run")
            if returncode == 128:
                mock_run.side_effect = CalledProcessError(returncode, "git merge-base")
            else:
                mock_result = MagicMock()
                mock_result.returncode = returncode
                mock_run.return_value = mock_result

            result = git_merge_base("main", "feature")

            mock_run.assert_called_once_with(
                ["git", "merge-base", "--is-ancestor", "main", "feature"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                encoding="utf-8",
            )

            assert result == expected

    class TestGetCommitInfo:
        @pytest.mark.benchmark
        @pytest.mark.parametrize(
            "format_string",
            [
                ("s"),
                ("an"),
                ("ae"),
            ],
        )
        @pytest.mark.benchmark
        def test_get_commit_info(self, mocker, format_string):
            # Must call get_commit_info with given argument when there are commits.
            mocker.patch("commit_check.util.has_commits", return_value=True)
            m_cmd_output = mocker.patch(
                "commit_check.util.cmd_output", return_value=" fake commit message "
            )
            retval = get_commit_info(format_string)
            assert m_cmd_output.call_count == 1
            assert m_cmd_output.call_args[0][0] == [
                "git",
                "log",
                "-n",
                "1",
                f"--pretty=format:%{format_string}",
                "HEAD",
            ]
            assert retval == " fake commit message "

        @pytest.mark.benchmark
        def test_get_commit_info_no_commits(self, mocker):
            # Must return 'Repo has no commits yet.' when there are no commits.
            mocker.patch("commit_check.util.has_commits", return_value=False)
            mocker.patch(
                "commit_check.util.cmd_output", return_value=" fake commit message "
            )
            format_string = "s"
            retval = get_commit_info(format_string)
            assert retval == " fake commit message "

        @pytest.mark.benchmark
        def test_get_commit_info_with_exception(self, mocker):
            # Must return empty string when exception raises in cmd_output.
            mocker.patch("commit_check.util.has_commits", return_value=True)
            m_cmd_output = mocker.patch(
                "commit_check.util.cmd_output", return_value=" fake commit message "
            )
            # CalledProcessError's args also dummy
            dummy_ret_code, dummy_cmd_name = 1, "dcmd"
            m_cmd_output.side_effect = CalledProcessError(
                dummy_ret_code, dummy_cmd_name
            )
            format_string = "s"
            retval = get_commit_info(format_string)
            assert m_cmd_output.call_count == 1
            assert m_cmd_output.call_args[0][0] == [
                "git",
                "log",
                "-n",
                "1",
                f"--pretty=format:%{format_string}",
                "HEAD",
            ]
            assert retval == ""

    class TestCmdOutput:
        # use DummyProcessResult in this test to access returncode, stdout and stderr attribute
        class DummyProcessResult:
            def __init__(self, returncode, stdout, stderr):
                self.returncode = returncode
                self.stdout = stdout
                self.stderr = stderr

        @pytest.mark.benchmark
        def test_cmd_output(self, mocker):
            # Must subprocess.run with given argument.
            m_subprocess_run = mocker.patch(
                "subprocess.run", return_value=self.DummyProcessResult(0, "ok", "")
            )
            retval = cmd_output(["dummy_cmd"])
            assert m_subprocess_run.call_count == 1
            assert retval == "ok"

        @pytest.mark.benchmark
        @pytest.mark.parametrize(
            "returncode, stdout, stderr",
            [
                (1, "ok", "err"),
                (0, None, "err"),
                (1, None, "err"),
            ],
        )
        @pytest.mark.benchmark
        def test_cmd_output_err(self, mocker, returncode, stdout, stderr):
            # Must return stderr when  subprocess.run returns not empty stderr.
            m_subprocess_run = mocker.patch(
                "subprocess.run",
                return_value=self.DummyProcessResult(returncode, stdout, stderr),
            )
            dummy_cmd = ["dummy_cmd"]
            retval = cmd_output(dummy_cmd)
            assert m_subprocess_run.call_count == 1
            assert retval == stderr
            assert m_subprocess_run.call_args[0][0] == dummy_cmd
            assert m_subprocess_run.call_args[1] == {
                "encoding": "utf-8",
                "stderr": PIPE,
                "stdout": PIPE,
            }

        @pytest.mark.benchmark
        @pytest.mark.parametrize(
            "returncode, stdout, stderr",
            [
                (1, "ok", ""),
                (0, None, ""),
                (1, None, ""),
            ],
        )
        @pytest.mark.benchmark
        def test_cmd_output_err_with_len0_stderr(
            self, mocker, returncode, stdout, stderr
        ):
            # Must return empty string when subprocess.run returns empty stderr.
            m_subprocess_run = mocker.patch(
                "subprocess.run",
                return_value=self.DummyProcessResult(returncode, stdout, stderr),
            )
            dummy_cmd = ["dummy_cmd"]
            retval = cmd_output(dummy_cmd)
            assert m_subprocess_run.call_count == 1
            assert retval == ""
            assert m_subprocess_run.call_args[0][0] == dummy_cmd
            assert m_subprocess_run.call_args[1] == {
                "encoding": "utf-8",
                "stderr": PIPE,
                "stdout": PIPE,
            }

    class TestPrintErrorMessage:
        @pytest.mark.benchmark
        def test_print_error_header(self, capfd):
            # Must print on stdout with given argument.
            print_error_header()
            stdout, _ = capfd.readouterr()
            assert "Commit rejected by Commit-Check" in stdout
            assert "Commit rejected." in stdout

        @pytest.mark.benchmark
        @pytest.mark.parametrize(
            "check_type, printed_name",
            [
                ("message", "message"),
                ("branch", "branch"),
                # The config key is snake_case, but the rules reference titles
                # its sections in kebab-case. The output follows the reference,
                # so a name read here can be searched for there verbatim.
                ("author_name", "author-name"),
                ("author_email", "author-email"),
                ("signoff", "signoff"),
            ],
        )
        @pytest.mark.benchmark
        def test_print_error_message(self, capfd, check_type, printed_name):
            # Must print on stdout with given argument.
            dummy_reason = "failure reason"
            dummy_error = "dummy error"
            print_error_message(check_type, dummy_error, dummy_reason)
            stdout, _ = capfd.readouterr()
            assert printed_name in stdout
            assert "_" not in stdout.split(" check failed")[0]
            assert "check failed ==>" in stdout
            assert dummy_error in stdout

    class TestHyperlinks:
        """The rule ID doubles as a link to its documentation.

        Only where that renders: a terminal that does not understand OSC 8 may
        print the escape payload as visible junk, and in a CI log the sequence
        is noise around a URL the reader can no longer click anyway.
        """

        @pytest.mark.benchmark
        def test_hyperlink_wraps_text_in_osc8(self):
            assert hyperlink("CC001", "https://example.com/#cc001") == (
                "\033]8;;https://example.com/#cc001\033\\CC001\033]8;;\033\\"
            )

        @pytest.mark.benchmark
        def test_not_supported_when_piped(self, mocker):
            mocker.patch.dict("os.environ", {}, clear=True)
            mocker.patch("sys.stdout.isatty", return_value=False)
            assert supports_hyperlinks() is False

        @pytest.mark.benchmark
        def test_forced_even_when_piped(self, mocker):
            mocker.patch.dict("os.environ", {"FORCE_HYPERLINK": "1"}, clear=True)
            mocker.patch("sys.stdout.isatty", return_value=False)
            assert supports_hyperlinks() is True

        @pytest.mark.benchmark
        def test_force_zero_turns_links_off(self, mocker):
            """Setting it to 0 must not read as "set, therefore on"."""
            mocker.patch.dict(
                "os.environ",
                {"FORCE_HYPERLINK": "0", "TERM_PROGRAM": "WezTerm"},
                clear=True,
            )
            mocker.patch("sys.stdout.isatty", return_value=True)
            assert supports_hyperlinks() is False

        @pytest.mark.benchmark
        def test_force_empty_falls_through_to_detection(self, mocker):
            mocker.patch.dict("os.environ", {"FORCE_HYPERLINK": ""}, clear=True)
            mocker.patch("sys.stdout.isatty", return_value=False)
            assert supports_hyperlinks() is False

        @pytest.mark.benchmark
        @pytest.mark.parametrize(
            "env, expected",
            [
                ({"TERM_PROGRAM": "WezTerm"}, True),
                ({"TERM_PROGRAM": "iTerm.app"}, True),
                ({"TERM_PROGRAM": "vscode"}, True),
                ({"TERM": "xterm-kitty"}, True),
                ({"VTE_VERSION": "6003"}, True),
                ({"VTE_VERSION": "4000"}, False),
                # Malformed rather than absent; must not raise.
                ({"VTE_VERSION": "not-a-number"}, False),
                ({"TERM": "xterm"}, False),
                ({"TERM": "dumb", "TERM_PROGRAM": "WezTerm"}, False),
            ],
        )
        def test_terminal_detection(self, mocker, env, expected):
            mocker.patch.dict("os.environ", env, clear=True)
            mocker.patch("sys.stdout.isatty", return_value=True)
            assert supports_hyperlinks() is expected

        @pytest.mark.benchmark
        def test_id_is_linked_when_supported(self, capfd, mocker):
            mocker.patch("commit_check.util.supports_hyperlinks", return_value=True)
            print_error_message(
                "subject_min_length",
                "too short",
                "hi",
                rule_id="CC005",
                docs_url="https://commit-check.com/rules/#cc005",
            )
            stdout, _ = capfd.readouterr()
            assert "\033]8;;https://commit-check.com/rules/#cc005\033\\" in stdout

        @pytest.mark.benchmark
        def test_id_is_plain_when_unsupported(self, capfd, mocker):
            mocker.patch("commit_check.util.supports_hyperlinks", return_value=False)
            print_error_message(
                "subject_min_length",
                "too short",
                "hi",
                rule_id="CC005",
                docs_url="https://commit-check.com/rules/#cc005",
            )
            stdout, _ = capfd.readouterr()
            assert "\033]8;;" not in stdout
            assert "CC005" in stdout

        @pytest.mark.benchmark
        def test_docs_line_kept_without_hyperlinks(self, capfd, mocker):
            """A CI log is where the printed URL is the only way to reach it."""
            mocker.patch("commit_check.util.supports_hyperlinks", return_value=False)
            _print_failure(
                {
                    "check": "subject_min_length",
                    "error": "too short",
                    "suggest": "write more",
                    "rule_id": "CC005",
                    "docs_url": "https://commit-check.com/rules/#cc005",
                },
                "hi",
                no_banner=True,
            )
            stdout, _ = capfd.readouterr()
            assert "Docs: https://commit-check.com/rules/#cc005" in stdout

        @pytest.mark.benchmark
        def test_docs_line_dropped_when_id_is_the_link(self, capfd, mocker):
            """Otherwise every failure spends a line repeating its own link."""
            mocker.patch("commit_check.util.supports_hyperlinks", return_value=True)
            _print_failure(
                {
                    "check": "subject_min_length",
                    "error": "too short",
                    "suggest": "write more",
                    "rule_id": "CC005",
                    "docs_url": "https://commit-check.com/rules/#cc005",
                },
                "hi",
                no_banner=True,
            )
            stdout, _ = capfd.readouterr()
            assert "Docs: " not in stdout
            assert "\033]8;;" in stdout

        @pytest.mark.benchmark
        def test_blank_line_closes_the_block(self, capfd, mocker):
            """The separator belongs between rules, not inside one."""
            mocker.patch("commit_check.util.supports_hyperlinks", return_value=False)
            _print_failure(
                {
                    "check": "subject_min_length",
                    "error": "too short",
                    "suggest": "write more",
                    "rule_id": "CC005",
                    "docs_url": "https://commit-check.com/rules/#cc005",
                },
                "hi",
                no_banner=True,
            )
            stdout, _ = capfd.readouterr()
            lines = stdout.splitlines()
            assert lines[-1] == ""
            assert lines[-2].startswith("Docs: ")
            assert "" not in lines[:-1]

    class TestPrintSuggestion:
        @pytest.mark.benchmark
        def test_print_suggestion(self, capfd):
            # Must print on stdout with given argument.
            print_suggestion("dummy suggest")
            stdout, _ = capfd.readouterr()
            assert "Suggest:" in stdout


class TestGetGitConfigValue:
    """Tests for get_git_config_value utility function."""

    @pytest.mark.benchmark
    def test_get_git_config_value_success(self, mocker):
        """Test getting a git config value successfully."""
        from commit_check.util import get_git_config_value

        mocker.patch("commit_check.util.cmd_output", return_value="John Doe\n")
        result = get_git_config_value(USER_NAME_CONFIG)
        assert result == "John Doe"

    @pytest.mark.benchmark
    def test_get_git_config_value_not_set(self, mocker):
        """Test getting a git config value when it is not set."""
        from commit_check.util import get_git_config_value

        mocker.patch("commit_check.util.cmd_output", return_value="")
        result = get_git_config_value(USER_NAME_CONFIG)
        assert result == ""

    @pytest.mark.benchmark
    def test_get_git_config_value_exception(self, mocker):
        """Test get_git_config_value returns empty string on CalledProcessError."""
        from commit_check.util import get_git_config_value

        mocker.patch(
            "commit_check.util.cmd_output",
            side_effect=CalledProcessError(
                returncode=1, cmd="git config --get user.name"
            ),
        )
        result = get_git_config_value(USER_NAME_CONFIG)
        assert result == ""

    @pytest.mark.benchmark
    def test_get_git_config_value_email(self, mocker):
        """Test getting user.email from git config."""
        from commit_check.util import get_git_config_value

        mocker.patch("commit_check.util.cmd_output", return_value="alice@example.com\n")
        result = get_git_config_value("user.email")
        assert result == "alice@example.com"
