import base64
import os
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import git
import pytest

from cecli.coders import Coder
from cecli.coders.base_coder import FinishReasonLength, UnknownEditFormat
from cecli.commands import SwitchCoderSignal
from cecli.dump import dump  # noqa: F401
from cecli.helpers.conversation import ConversationChunks
from cecli.helpers.conversation.manager import ConversationManager
from cecli.helpers.conversation.tags import MessageTag
from cecli.io import InputOutput
from cecli.mcp import McpServerManager
from cecli.models import Model
from cecli.repo import GitRepo
from cecli.sendchat import sanity_check_messages
from cecli.utils import GitTemporaryDirectory


class TestCoder:
    @pytest.fixture(autouse=True)
    def setup(self, gpt35_model):
        self.GPT35 = gpt35_model
        self.webbrowser_patcher = patch("cecli.io.webbrowser.open")
        self.mock_webbrowser = self.webbrowser_patcher.start()
        # Reset conversation system before each test
        ConversationChunks.reset()

        yield

        # Cleanup after each test
        self.webbrowser_patcher.stop()
        # Reset conversation system after each test as well
        ConversationChunks.reset()

    async def test_allowed_to_edit(self):
        with GitTemporaryDirectory():
            repo = git.Repo()

            fname = Path("added.txt")
            fname.touch()
            repo.git.add(str(fname))

            fname = Path("repo.txt")
            fname.touch()
            repo.git.add(str(fname))

            repo.git.commit("-m", "init")

            # YES!
            # Use a completely mocked IO object instead of a real one
            io = MagicMock()
            io.confirm_ask = AsyncMock(return_value=True)
            coder = await Coder.create(self.GPT35, None, io, fnames=["added.txt"])

            assert await coder.allowed_to_edit("added.txt")
            assert await coder.allowed_to_edit("repo.txt")
            assert await coder.allowed_to_edit("new.txt")

            assert "repo.txt" in str(coder.abs_fnames)
            assert "new.txt" in str(coder.abs_fnames)

            assert not coder.need_commit_before_edits

    async def test_allowed_to_edit_no(self):
        with GitTemporaryDirectory():
            repo = git.Repo()

            fname = Path("added.txt")
            fname.touch()
            repo.git.add(str(fname))

            fname = Path("repo.txt")
            fname.touch()
            repo.git.add(str(fname))

            repo.git.commit("-m", "init")

            io = InputOutput(yes=False)
            io.confirm_ask = AsyncMock(return_value=False)

            coder = await Coder.create(self.GPT35, None, io, fnames=["added.txt"])

            assert await coder.allowed_to_edit("added.txt")
            assert not await coder.allowed_to_edit("repo.txt")
            assert not await coder.allowed_to_edit("new.txt")

            assert "repo.txt" not in str(coder.abs_fnames)
            assert "new.txt" not in str(coder.abs_fnames)

            assert not coder.need_commit_before_edits

    async def test_allowed_to_edit_dirty(self):
        with GitTemporaryDirectory():
            repo = git.Repo()

            fname = Path("added.txt")
            fname.touch()
            repo.git.add(str(fname))

            repo.git.commit("-m", "init")

            # say NO
            io = InputOutput(yes=False)

            coder = await Coder.create(self.GPT35, None, io, fnames=["added.txt"])

            assert await coder.allowed_to_edit("added.txt")
            assert not coder.need_commit_before_edits

            fname.write_text("dirty!")
            assert await coder.allowed_to_edit("added.txt")
            assert coder.need_commit_before_edits

    async def test_get_files_content(self):
        tempdir = Path(tempfile.mkdtemp())

        file1 = tempdir / "file1.txt"
        file2 = tempdir / "file2.txt"

        file1.touch()
        file2.touch()

        files = [file1, file2]

        # Initialize the Coder object with the mocked IO and mocked repo
        coder = await Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)

        content = coder.get_files_content()
        all_file_names = content["chat_file_names"] | content["edit_file_names"]
        assert "file1.txt" in all_file_names
        assert "file2.txt" in all_file_names

    async def test_check_for_filename_mentions(self):
        with GitTemporaryDirectory():
            repo = git.Repo()

            mock_io = MagicMock()
            mock_io.confirm_ask = AsyncMock(return_value=True)

            fname1 = Path("file1.txt")
            fname2 = Path("file2.py")

            fname1.write_text("one\n")
            fname2.write_text("two\n")

            repo.git.add(str(fname1))
            repo.git.add(str(fname2))
            repo.git.commit("-m", "new")

            mock_args = MagicMock(tui=False)
            coder = await Coder.create(self.GPT35, None, mock_io, args=mock_args)

            await coder.check_for_file_mentions("Please check file1.txt and file2.py")

            expected_files = set(
                [
                    str(Path(coder.root) / fname1),
                    str(Path(coder.root) / fname2),
                ]
            )

            assert coder.abs_fnames == expected_files

    async def test_check_for_ambiguous_filename_mentions_of_longer_paths(self):
        with GitTemporaryDirectory():
            io = InputOutput(pretty=False, yes=True)
            mock_args = MagicMock(tui=False)
            coder = await Coder.create(self.GPT35, None, io, args=mock_args)

            fname = Path("file1.txt")
            fname.touch()

            other_fname = Path("other") / "file1.txt"
            other_fname.parent.mkdir(parents=True, exist_ok=True)
            other_fname.touch()

            mock = MagicMock()
            mock.return_value = set([str(fname), str(other_fname)])
            coder.repo.get_tracked_files = mock

            await coder.check_for_file_mentions(f"Please check {fname}!")

            assert coder.abs_fnames == {str(fname.resolve())}

    async def test_skip_duplicate_basename_mentions(self):
        with GitTemporaryDirectory():
            io = InputOutput(pretty=False, yes=True)
            coder = await Coder.create(self.GPT35, None, io)

            # Create files with same basename in different directories
            fname1 = Path("dir1") / "file.txt"
            fname2 = Path("dir2") / "file.txt"
            fname3 = Path("dir3") / "unique.txt"

            for fname in [fname1, fname2, fname3]:
                fname.parent.mkdir(parents=True, exist_ok=True)
                fname.touch()

            # Add one file to chat
            coder.add_rel_fname(str(fname1))

            # Mock get_tracked_files to return all files
            mock = MagicMock()
            mock.return_value = set([str(fname1), str(fname2), str(fname3)])
            coder.repo.get_tracked_files = mock

            # Check that file mentions of a pure basename skips files with duplicate basenames
            mentioned = coder.get_file_mentions(f"Check {fname2.name} and {fname3}")
            assert mentioned == {str(fname3)}

            # Add a read-only file with same basename
            coder.abs_read_only_fnames.add(str(fname2.resolve()))
            mentioned = coder.get_file_mentions(f"Check {fname1} and {fname3}")
            assert mentioned == {str(fname3)}

    async def test_check_for_file_mentions_read_only(self):
        with GitTemporaryDirectory():
            io = InputOutput(pretty=False, yes=True)
            coder = await Coder.create(self.GPT35, None, io)

            fname = Path("readonly_file.txt")
            fname.touch()

            coder.abs_read_only_fnames.add(str(fname.resolve()))

            mock = MagicMock()
            mock.return_value = set([str(fname)])
            coder.repo.get_tracked_files = mock

            result = await coder.check_for_file_mentions(f"Please check {fname}!")

            assert result is None
            assert coder.abs_fnames == set()

    async def test_check_for_file_mentions_with_mocked_confirm(self):
        with GitTemporaryDirectory():
            io = InputOutput(pretty=False)
            io.confirm_ask = AsyncMock(side_effect=[False, True, True])
            mock_args = MagicMock(tui=False)
            coder = await Coder.create(self.GPT35, None, io, args=mock_args)

            coder.get_file_mentions = MagicMock(return_value=set(["file1.txt", "file2.txt"]))

            await coder.check_for_file_mentions("Please check file1.txt for the info")

            assert io.confirm_ask.call_count == 2
            assert len(coder.abs_fnames) == 1
            assert "file2.txt" in str(coder.abs_fnames)

            io.confirm_ask.reset_mock()

            await coder.check_for_file_mentions("Please check file1.txt and file2.txt again")

            assert io.confirm_ask.call_count == 1
            assert len(coder.abs_fnames) == 1
            assert "file2.txt" in str(coder.abs_fnames)
            assert "file1.txt" in coder.ignore_mentions

    async def test_check_for_subdir_mention(self):
        with GitTemporaryDirectory():
            io = InputOutput(pretty=False, yes=True)
            mock_args = MagicMock(tui=False)
            coder = await Coder.create(self.GPT35, None, io, args=mock_args)

            fname = Path("other") / "file1.txt"
            fname.parent.mkdir(parents=True, exist_ok=True)
            fname.touch()

            mock = MagicMock()
            mock.return_value = set([str(fname)])
            coder.repo.get_tracked_files = mock

            await coder.check_for_file_mentions(f"Please check `{fname}`")

            assert coder.abs_fnames == {str(fname.resolve())}

    async def test_get_file_mentions_various_formats(self):
        with GitTemporaryDirectory():
            io = InputOutput(pretty=False, yes=True)
            coder = await Coder.create(self.GPT35, None, io)

            # Create test files
            test_files = [
                "file1.txt",
                "file2.py",
                "dir/nested_file.js",
                "dir/subdir/deep_file.html",
                "file99.txt",
                "special_chars!@#.md",
            ]

            # Pre-format the Windows path to avoid backslash issues in f-string expressions
            windows_path = test_files[2].replace("/", "\\")
            win_path3 = test_files[3].replace("/", "\\")

            for fname in test_files:
                fpath = Path(fname)
                fpath.parent.mkdir(parents=True, exist_ok=True)
                fpath.touch()

            # Mock get_addable_relative_files to return our test files
            coder.get_addable_relative_files = MagicMock(return_value=set(test_files))

            # Test different mention formats
            test_cases = [
                # Simple plain text mentions
                (f"You should edit {test_files[0]} first", {test_files[0]}),
                # Multiple files in plain text
                (
                    f"Edit both {test_files[0]} and {test_files[1]}",
                    {test_files[0], test_files[1]},
                ),
                # Files in backticks
                (f"Check the file `{test_files[2]}`", {test_files[2]}),
                # Files in code blocks
                (f"```\n{test_files[3]}\n```", {test_files[3]}),
                # Files in code blocks with language specifier
                # (
                #    f"```python\nwith open('{test_files[1]}', 'r') as f:\n"
                #    f"    data = f.read()\n```",
                #    {test_files[1]},
                # ),
                # Files with Windows-style paths
                (f"Edit the file {windows_path}", {test_files[2]}),
                # Files with different quote styles
                (f'Check "{test_files[5]}" now', {test_files[5]}),
                # All files in one complex message
                (
                    (
                        f"First, edit `{test_files[0]}`. Then modify {test_files[1]}.\n"
                        f"```js\n// Update this file\nconst file = '{test_files[2]}';\n```\n"
                        f"Finally check {win_path3}"
                    ),
                    {test_files[0], test_files[1], test_files[2], test_files[3]},
                ),
                # Files mentioned in markdown bold format
                (f"You should check **{test_files[0]}** for issues", {test_files[0]}),
                (
                    f"Look at both **{test_files[1]}** and **{test_files[2]}**",
                    {test_files[1], test_files[2]},
                ),
                (
                    f"The file **{win_path3}** needs updating",
                    {test_files[3]},
                ),
                (
                    f"Files to modify:\n- **{test_files[0]}**\n- **{test_files[4]}**",
                    {test_files[0], test_files[4]},
                ),
            ]

            for content, expected_mentions in test_cases:
                mentioned_files = coder.get_file_mentions(content)
                assert (
                    mentioned_files == expected_mentions
                ), f"Failed to extract mentions from: {content}"

    async def test_get_file_mentions_multiline_backticks(self):
        with GitTemporaryDirectory():
            io = InputOutput(pretty=False, yes=True)
            coder = await Coder.create(self.GPT35, None, io)

            # Create test files
            test_files = [
                "swebench/harness/test_spec/python.py",
                "swebench/harness/test_spec/javascript.py",
            ]
            for fname in test_files:
                fpath = Path(fname)
                fpath.parent.mkdir(parents=True, exist_ok=True)
                fpath.touch()

            # Mock get_addable_relative_files to return our test files
            coder.get_addable_relative_files = MagicMock(return_value=set(test_files))

            # Input text with multiline backticked filenames
            content = """
Could you please **add the following files to the chat**?

1.  `swebench/harness/test_spec/python.py`
2.  `swebench/harness/test_spec/javascript.py`

Once I have these, I can show you precisely how to do the thing.
"""
            expected_mentions = {
                "swebench/harness/test_spec/python.py",
                "swebench/harness/test_spec/javascript.py",
            }

            mentioned_files = coder.get_file_mentions(content)
            assert (
                mentioned_files == expected_mentions
            ), f"Failed to extract mentions from multiline backticked content: {content}"

    async def test_get_file_mentions_path_formats(self):
        with GitTemporaryDirectory():
            io = InputOutput(pretty=False, yes=True)
            coder = await Coder.create(self.GPT35, None, io)

            # Test cases with different path formats
            test_cases = [
                # Unix paths in content, Unix paths in get_addable_relative_files
                ("Check file1.txt and dir/file2.txt", ["file1.txt", "dir/file2.txt"]),
                # Windows paths in content, Windows paths in get_addable_relative_files
                ("Check file1.txt and dir\\file2.txt", ["file1.txt", "dir\\file2.txt"]),
                # Unix paths in content, Windows paths in get_addable_relative_files
                ("Check file1.txt and dir/file2.txt", ["file1.txt", "dir\\file2.txt"]),
                # Windows paths in content, Unix paths in get_addable_relative_files
                ("Check file1.txt and dir\\file2.txt", ["file1.txt", "dir/file2.txt"]),
                # Mixed paths in content, Unix paths in get_addable_relative_files
                (
                    "Check file1.txt, dir/file2.txt, and other\\file3.txt",
                    ["file1.txt", "dir/file2.txt", "other/file3.txt"],
                ),
                # Mixed paths in content, Windows paths in get_addable_relative_files
                (
                    "Check file1.txt, dir/file2.txt, and other\\file3.txt",
                    ["file1.txt", "dir\\file2.txt", "other\\file3.txt"],
                ),
            ]

            for content, addable_files in test_cases:
                coder.get_addable_relative_files = MagicMock(return_value=set(addable_files))
                mentioned_files = coder.get_file_mentions(content)
                expected_files = set(addable_files)
                assert (
                    mentioned_files == expected_files
                ), f"Failed for content: {content}, addable_files: {addable_files}"

    async def test_run_with_file_deletion(self):
        # Create a few temporary files

        tempdir = Path(tempfile.mkdtemp())

        file1 = tempdir / "file1.txt"
        file2 = tempdir / "file2.txt"

        file1.touch()
        file2.touch()

        files = [file1, file2]

        coder = await Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)

        async def mock_send(*args, **kwargs):
            coder.partial_response_content = "ok"
            coder.partial_response_function_call = dict()
            coder.partial_response_chunks = []
            return
            yield

        coder.send = mock_send

        # Call the run method with a message
        await coder.run(with_message="hi")
        assert len(coder.abs_fnames) == 2

        file1.unlink()

        # Call the run method again with a message
        await coder.run(with_message="hi")
        assert len(coder.abs_fnames) == 1

    async def test_run_with_file_unicode_error(self):
        # Create a few temporary files
        _, file1 = tempfile.mkstemp()
        _, file2 = tempfile.mkstemp()

        files = [file1, file2]

        coder = await Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)

        async def mock_send(*args, **kwargs):
            coder.partial_response_content = "ok"
            coder.partial_response_function_call = dict()
            coder.partial_response_chunks = []
            return
            yield

        coder.send = mock_send

        # Call the run method with a message
        await coder.run(with_message="hi")
        assert len(coder.abs_fnames) == 2

        # Write some non-UTF8 text into the file
        with open(file1, "wb") as f:
            f.write(b"\x80abc")

        # Call the run method again with a message
        await coder.run(with_message="hi")
        assert len(coder.abs_fnames) == 1

    async def test_choose_fence(self):
        # Create a few temporary files
        _, file1 = tempfile.mkstemp()

        with open(file1, "wb") as f:
            f.write(b"this contains\n```\nbackticks")

        files = [file1]

        coder = await Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)

        async def mock_send(*args, **kwargs):
            coder.partial_response_content = "ok"
            coder.partial_response_function_call = dict()
            coder.partial_response_chunks = []
            return
            yield

        coder.send = mock_send

        # Call the run method with a message
        await coder.run(with_message="hi")

        assert coder.fence[0] != "```"

    async def test_run_with_file_utf_unicode_error(self):
        "make sure that we honor InputOutput(encoding) and don't just assume utf-8"
        encoding = "utf-16"
        _, file1 = tempfile.mkstemp()
        _, file2 = tempfile.mkstemp()
        files = [file1, file2]

        # Initialize the Coder object with the mocked IO and mocked repo
        coder = await Coder.create(
            self.GPT35,
            None,
            io=InputOutput(encoding=encoding),
            fnames=files,
        )

        async def mock_send(*args, **kwargs):
            coder.partial_response_content = "ok"
            coder.partial_response_function_call = dict()
            coder.partial_response_chunks = []
            return
            yield

        coder.send = mock_send

        # Call the run method with a message
        await coder.run(with_message="hi")
        assert len(coder.abs_fnames) == 2

        some_content_which_will_error_if_read_with_encoding_utf8 = "ÅÍÎÏ".encode(encoding)
        with open(file1, "wb") as f:
            f.write(some_content_which_will_error_if_read_with_encoding_utf8)

        await coder.run(with_message="hi")

        # both files should still be here
        assert len(coder.abs_fnames) == 2

    async def test_new_file_edit_one_commit(self):
        """A new file should get pre-committed before the GPT edit commit"""
        with GitTemporaryDirectory():
            repo = git.Repo()

            fname = Path("file.txt")

            io = InputOutput(yes=True)
            io.tool_warning = MagicMock()
            coder = await Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])

            assert fname.exists()

            # make sure it was not committed
            with pytest.raises(git.exc.GitCommandError):
                list(repo.iter_commits(repo.active_branch.name))

            async def mock_send(*args, **kwargs):
                coder.partial_response_content = f"""
Do this:

{str(fname)}
<<<<<<< SEARCH
=======
new
>>>>>>> REPLACE

"""
                coder.partial_response_function_call = dict()
                coder.partial_response_chunks = []
                return
                yield

            coder.send = mock_send
            coder.repo.get_commit_message = AsyncMock(return_value="commit message")

            await coder.run(with_message="hi")

            content = fname.read_text()
            assert content == "new\n"

            num_commits = len(list(repo.iter_commits(repo.active_branch.name)))
            assert num_commits == 2

    async def test_only_commit_gpt_edited_file(self):
        """
        Only commit file that gpt edits, not other dirty files.
        Also ensure commit msg only depends on diffs from the GPT edited file.
        """

        with GitTemporaryDirectory():
            repo = git.Repo()

            fname1 = Path("file1.txt")
            fname2 = Path("file2.txt")

            fname1.write_text("one\n")
            fname2.write_text("two\n")

            repo.git.add(str(fname1))
            repo.git.add(str(fname2))
            repo.git.commit("-m", "new")

            # DIRTY!
            fname1.write_text("ONE\n")

            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname1), str(fname2)])

            async def mock_send(*args, **kwargs):
                coder.partial_response_content = f"""
Do this:

{str(fname2)}
<<<<<<< SEARCH
two
=======
TWO
>>>>>>> REPLACE

"""
                coder.partial_response_function_call = dict()
                return
                yield

            def mock_get_commit_message(diffs, context, user_language=None):
                assert "one" not in diffs
                assert "ONE" not in diffs
                return "commit message"

            coder.send = mock_send
            coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message)

            await coder.run(with_message="hi")

            content = fname2.read_text()
            assert content == "TWO\n"

            assert repo.is_dirty(path=str(fname1))

    async def test_gpt_edit_to_dirty_file(self):
        """A dirty file should be committed before the GPT edits are committed"""

        with GitTemporaryDirectory():
            repo = git.Repo()

            fname = Path("file.txt")
            fname.write_text("one\n")
            repo.git.add(str(fname))

            fname2 = Path("other.txt")
            fname2.write_text("other\n")
            repo.git.add(str(fname2))

            repo.git.commit("-m", "new")

            # dirty
            fname.write_text("two\n")
            fname2.write_text("OTHER\n")

            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])

            async def mock_send(*args, **kwargs):
                coder.partial_response_content = f"""
Do this:

{str(fname)}
<<<<<<< SEARCH
two
=======
three
>>>>>>> REPLACE

"""
                coder.partial_response_function_call = dict()
                coder.partial_response_chunks = []
                return
                yield

            saved_diffs = []

            async def mock_get_commit_message(diffs, context, user_language=None):
                saved_diffs.append(diffs)
                return "commit message"

            coder.repo.get_commit_message = mock_get_commit_message
            coder.send = mock_send

            await coder.run(with_message="hi")

            content = fname.read_text()
            assert content == "three\n"

            num_commits = len(list(repo.iter_commits(repo.active_branch.name)))
            assert num_commits == 3

            diff = repo.git.diff(["HEAD~2", "HEAD~1"])
            assert "one" in diff
            assert "two" in diff
            assert "three" not in diff
            assert "other" not in diff
            assert "OTHER" not in diff

            diff = saved_diffs[0]
            assert "one" in diff
            assert "two" in diff
            assert "three" not in diff
            assert "other" not in diff
            assert "OTHER" not in diff

            diff = repo.git.diff(["HEAD~1", "HEAD"])
            assert "one" not in diff
            assert "two" in diff
            assert "three" in diff
            assert "other" not in diff
            assert "OTHER" not in diff

            diff = saved_diffs[1]
            assert "one" not in diff
            assert "two" in diff
            assert "three" in diff
            assert "other" not in diff
            assert "OTHER" not in diff

            assert len(saved_diffs) == 2

    async def test_gpt_edit_to_existing_file_not_in_repo(self):
        with GitTemporaryDirectory():
            repo = git.Repo()

            fname = Path("file.txt")
            fname.write_text("one\n")

            fname2 = Path("other.txt")
            fname2.write_text("other\n")
            repo.git.add(str(fname2))

            repo.git.commit("-m", "initial")

            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])

            async def mock_send(*args, **kwargs):
                coder.partial_response_content = f"""
Do this:

{str(fname)}
<<<<<<< SEARCH
one
=======
two
>>>>>>> REPLACE

"""
                coder.partial_response_function_call = dict()
                coder.partial_response_chunks = []
                return
                yield

            saved_diffs = []

            async def mock_get_commit_message(diffs, context, user_language=None):
                saved_diffs.append(diffs)
                return "commit message"

            coder.repo.get_commit_message = mock_get_commit_message
            coder.send = mock_send

            await coder.run(with_message="hi")

            content = fname.read_text()
            assert content == "two\n"

            diff = saved_diffs[0]
            assert "file.txt" in diff

    async def test_skip_cecli_ignored_files(self):
        with GitTemporaryDirectory():
            repo = git.Repo()

            fname1 = "ignoreme1.txt"
            fname2 = "ignoreme2.txt"
            fname3 = "dir/ignoreme3.txt"

            Path(fname2).touch()
            repo.git.add(str(fname2))
            repo.git.commit("-m", "initial")

            io = InputOutput(yes=True)

            fnames = [fname1, fname2, fname3]

            aignore = Path("cecli.ignore")
            aignore.write_text(f"{fname1}\n{fname2}\ndir\n")
            repo = GitRepo(
                io,
                fnames,
                None,
                cecli_ignore_file=str(aignore),
            )

            coder = await Coder.create(
                self.GPT35,
                None,
                io,
                fnames=fnames,
                repo=repo,
            )

            assert fname1 not in str(coder.abs_fnames)
            assert fname2 not in str(coder.abs_fnames)
            assert fname3 not in str(coder.abs_fnames)

    async def test_skip_gitignored_files_on_init(self):
        with GitTemporaryDirectory() as _:
            repo_path = Path(".")
            repo = git.Repo.init(repo_path)

            ignored_file = repo_path / "ignored_by_git.txt"
            ignored_file.write_text("This file should be ignored by git.")

            regular_file = repo_path / "regular_file.txt"
            regular_file.write_text("This is a regular file.")

            gitignore_content = "ignored_by_git.txt\n"
            (repo_path / ".gitignore").write_text(gitignore_content)

            repo.index.add([str(regular_file), ".gitignore"])
            repo.index.commit("Initial commit with gitignore and regular file")

            mock_io = MagicMock()
            mock_io.tool_warning = MagicMock()

            fnames_to_add = [str(ignored_file), str(regular_file)]

            coder = await Coder.create(self.GPT35, None, mock_io, fnames=fnames_to_add)

            assert str(ignored_file.resolve()) not in coder.abs_fnames
            assert str(regular_file.resolve()) in coder.abs_fnames
            mock_io.tool_warning.assert_any_call(
                f"Skipping {ignored_file.name} that matches gitignore spec."
            )

    async def test_check_for_urls(self):
        io = InputOutput(yes=True)
        mock_args = MagicMock()
        mock_args.yes_always_commands = False
        mock_args.disable_scraping = False
        coder = await Coder.create(self.GPT35, None, io=io, args=mock_args)

        # Mock the execute command to return scraped content
        async def mock_execute(cmd_name, url, **kwargs):
            if cmd_name == "web" and kwargs.get("return_content"):
                return f"Scraped content from {url}"
            return None

        coder.commands.execute = mock_execute

        # Test various URL formats
        test_cases = [
            ("Check http://example.com, it's cool", "http://example.com"),
            (
                "Visit https://www.example.com/page and see stuff",
                "https://www.example.com/page",
            ),
            (
                "Go to http://subdomain.example.com:8080/path?query=value, or not",
                "http://subdomain.example.com:8080/path?query=value",
            ),
            (
                "See https://example.com/path#fragment for example",
                "https://example.com/path#fragment",
            ),
            ("Look at http://localhost:3000", "http://localhost:3000"),
            (
                "View https://example.com/setup#whatever",
                "https://example.com/setup#whatever",
            ),
            ("Open http://127.0.0.1:8000/api/v1/", "http://127.0.0.1:8000/api/v1/"),
            (
                "Try https://example.com/path/to/page.html?param1=value1&param2=value2",
                "https://example.com/path/to/page.html?param1=value1&param2=value2",
            ),
            (
                "Access http://user:password@example.com",
                "http://user:password@example.com",
            ),
            (
                "Use https://example.com/path_(with_parentheses)",
                "https://example.com/path_(with_parentheses)",
            ),
        ]

        for input_text, expected_url in test_cases:
            result = await coder.check_for_urls(input_text)
            assert expected_url in result

        # Test cases from the GitHub issue
        issue_cases = [
            ("check http://localhost:3002, there is an error", "http://localhost:3002"),
            (
                "can you check out https://example.com/setup#whatever",
                "https://example.com/setup#whatever",
            ),
        ]

        for input_text, expected_url in issue_cases:
            result = await coder.check_for_urls(input_text)
            assert expected_url in result

        # Test case with multiple URLs
        multi_url_input = "Check http://example1.com and https://example2.com/page"
        result = await coder.check_for_urls(multi_url_input)
        assert "http://example1.com" in result
        assert "https://example2.com/page" in result

        # Test case with no URL
        no_url_input = "This text contains no URL"
        result = await coder.check_for_urls(no_url_input)
        assert result == no_url_input

        # Test case with the same URL appearing multiple times
        repeated_url_input = (
            "Check https://example.com, then https://example.com again, and https://example.com one"
            " more time"
        )
        result = await coder.check_for_urls(repeated_url_input)
        # the original 3 in the input text, plus 1 more for the scraped text
        assert result.count("https://example.com") == 4
        assert "https://example.com" in result

    async def test_coder_from_coder_with_subdir(self):
        with GitTemporaryDirectory() as root:
            repo = git.Repo.init(root)

            # Create a file in a subdirectory
            subdir = Path(root) / "subdir"
            subdir.mkdir()
            test_file = subdir / "test_file.txt"
            test_file.write_text("Test content")

            repo.git.add(str(test_file))
            repo.git.commit("-m", "Add test file")

            # Change directory to the subdirectory
            os.chdir(subdir.resolve())

            # Create the first coder
            io = InputOutput(yes=True)
            coder1 = await Coder.create(self.GPT35, None, io=io, fnames=[test_file.name])

            # Create a new coder from the first coder
            coder2 = await Coder.create(from_coder=coder1)

            # Check if both coders have the same set of abs_fnames
            assert coder1.abs_fnames == coder2.abs_fnames

            # Ensure the abs_fnames contain the correct absolute path
            expected_abs_path = os.path.realpath(str(test_file))
            coder1_abs_fnames = set(os.path.realpath(path) for path in coder1.abs_fnames)
            assert expected_abs_path in coder1_abs_fnames
            assert expected_abs_path in coder2.abs_fnames

            # Check that the abs_fnames do not contain duplicate or incorrect paths
            assert len(coder1.abs_fnames) == 1
            assert len(coder2.abs_fnames) == 1

    async def test_suggest_shell_commands(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            async def mock_send(*args, **kwargs):
                coder.partial_response_content = """Here's a shell command to run:

```bash
echo "Hello, World!"
```

This command will print 'Hello, World!' to the console."""
                coder.partial_response_function_call = dict()
                coder.partial_response_chunks = []
                return
                yield

            coder.send = mock_send

            # Mock the handle_shell_commands method to check if it's called
            coder.handle_shell_commands = AsyncMock()

            # Run the coder with a message
            await coder.run(with_message="Suggest a shell command")

            # Check if the shell command was added to the list
            assert len(coder.shell_commands) == 1
            assert coder.shell_commands[0].strip() == 'echo "Hello, World!"'

            # Check if handle_shell_commands was called with the correct argument
            coder.handle_shell_commands.assert_called_once()

    async def test_no_suggest_shell_commands(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io, suggest_shell_commands=False)
            assert not coder.suggest_shell_commands

    async def test_detect_urls_enabled(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            mock_args = MagicMock()
            mock_args.yes_always_commands = False
            mock_args.disable_scraping = False
            coder = await Coder.create(self.GPT35, "diff", io=io, detect_urls=True, args=mock_args)

            # Track calls to execute
            execute_calls = []

            async def mock_execute(cmd_name, url, **kwargs):
                execute_calls.append((cmd_name, url, kwargs))
                if cmd_name == "web" and kwargs.get("return_content"):
                    return f"Scraped content from {url}"
                return None

            coder.commands.execute = mock_execute

            # Test with a message containing a URL
            message = "Check out https://example.com"
            await coder.check_for_urls(message)

            # Verify execute was called with the web command and correct URL
            assert len(execute_calls) == 1
            assert execute_calls[0][0] == "web"
            assert execute_calls[0][1] == "https://example.com"
            assert execute_calls[0][2].get("return_content") is True

    async def test_detect_urls_disabled(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io, detect_urls=False)
            coder.commands.scraper = MagicMock()
            coder.commands.scraper.scrape = MagicMock(return_value="some content")

            # Test with a message containing a URL
            message = "Check out https://example.com"
            result = await coder.check_for_urls(message)
            assert result == message
            coder.commands.scraper.scrape.assert_not_called()

    def test_unknown_edit_format_exception(self):
        # Test the exception message format
        invalid_format = "invalid_format"
        valid_formats = ["diff", "whole", "map"]
        exc = UnknownEditFormat(invalid_format, valid_formats)
        expected_msg = (
            f"Unknown edit format {invalid_format}. Valid formats are: {', '.join(valid_formats)}"
        )
        assert str(exc) == expected_msg

    async def test_unknown_edit_format_creation(self):
        # Test that creating a Coder with invalid edit format raises the exception
        io = InputOutput(yes=True)
        invalid_format = "invalid_format"

        with pytest.raises(UnknownEditFormat) as cm:
            await Coder.create(self.GPT35, invalid_format, io=io)

        exc = cm.value
        assert exc.edit_format == invalid_format
        assert isinstance(exc.valid_formats, list)
        assert len(exc.valid_formats) > 0

    async def test_system_prompt_prefix(self):
        # Test that system_prompt_prefix is properly set and used
        io = InputOutput(yes=True)
        test_prefix = "Test prefix. "

        # Create a model with system_prompt_prefix
        model = Model("gpt-3.5-turbo")
        model.system_prompt_prefix = test_prefix

        coder = await Coder.create(model, None, io=io)

        # Get the formatted messages
        messages = coder.format_messages()

        # Check if the system message contains our prefix
        system_message = next(msg for msg in messages if msg["role"] == "system")
        assert system_message["content"].startswith(test_prefix)

    async def test_coder_create_with_new_file_oserror(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            new_file = "new_file.txt"

            # Mock Path.touch() to raise OSError
            with patch("pathlib.Path.touch", side_effect=OSError("Permission denied")):
                # Create the coder with a new file
                coder = await Coder.create(self.GPT35, "diff", io=io, fnames=[new_file])

            # Check if the coder was created successfully
            assert isinstance(coder, Coder)

            # Check if the new file is not in abs_fnames
            assert new_file not in [os.path.basename(f) for f in coder.abs_fnames]

    async def test_show_exhausted_error(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Set up some real done_messages and cur_messages using ConversationManager
            done_messages = [
                {
                    "role": "user",
                    "content": "Hello, can you help me with a Python problem?",
                },
                {
                    "role": "assistant",
                    "content": "Of course! I'd be happy to help. What's the problem you're facing?",
                },
                {
                    "role": "user",
                    "content": (
                        "I need to write a function that calculates the factorial of a number."
                    ),
                },
                {
                    "role": "assistant",
                    "content": (
                        "Sure, I can help you with that. Here's a simple Python function to"
                        " calculate the factorial of a number:"
                    ),
                },
            ]

            cur_messages = [
                {
                    "role": "user",
                    "content": "Can you optimize this function for large numbers?",
                },
            ]

            # Add messages to ConversationManager
            for msg in done_messages:
                ConversationManager.add_message(msg, MessageTag.DONE)

            for msg in cur_messages:
                ConversationManager.add_message(msg, MessageTag.CUR)

            # Set up real values for the main model
            coder.main_model.info = {
                "max_input_tokens": 4000,
                "max_output_tokens": 1000,
            }
            coder.partial_response_content = (
                "Here's an optimized version of the factorial function:"
            )
            coder.io.tool_error = MagicMock()

            # Call the method
            await coder.show_exhausted_error()

            # Check if tool_error was called with the expected message
            coder.io.tool_error.assert_called()
            error_message = coder.io.tool_error.call_args[0][0]

            # Assert that the error message contains the expected information
            assert "Model gpt-3.5-turbo has hit a token limit!" in error_message
            assert "Input tokens:" in error_message
            assert "Output tokens:" in error_message
            assert "Total tokens:" in error_message

    async def test_keyboard_interrupt_handling(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Simulate keyboard interrupt during message processing
            async def mock_send(*args, **kwargs):
                coder.partial_response_content = "Partial response"
                coder.partial_response_function_call = dict()
                coder.partial_response_chunks = []
                yield  # Make it an async generator
                raise KeyboardInterrupt()

            coder.send = mock_send

            # Initial valid state
            sanity_check_messages(coder.cur_messages)

            # Process message that will trigger interrupt
            async for _ in coder.send_message("Test message"):
                pass

            # Verify last message is from assistant
            # Note: sanity_check_messages would fail because keyboard interrupt adds
            # "^C KeyboardInterrupt" as a user message, creating two user messages in a row
            assert coder.cur_messages[-1]["role"] == "assistant"

    async def test_token_limit_error_handling(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Simulate token limit error
            async def mock_send(*args, **kwargs):
                coder.partial_response_content = "Partial response"
                coder.partial_response_function_call = dict()
                coder.partial_response_chunks = []
                yield  # Make it an async generator
                raise FinishReasonLength()

            coder.send = mock_send

            # Initial valid state
            sanity_check_messages(coder.cur_messages)

            # Process message that hits token limit
            async for _ in coder.send_message("Long message"):
                pass

            # Verify messages are still in valid state
            sanity_check_messages(coder.cur_messages)
            assert coder.cur_messages[-1]["role"] == "assistant"

    async def test_message_sanity_after_partial_response(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Simulate partial response then interrupt
            async def mock_send(*args, **kwargs):
                coder.partial_response_content = "Partial response"
                coder.partial_response_function_call = dict()
                coder.partial_response_chunks = []
                yield  # Make it an async generator
                raise KeyboardInterrupt()

            coder.send = mock_send

            async for _ in coder.send_message("Test"):
                pass

            # Verify last message is from assistant
            # Note: sanity_check_messages would fail because keyboard interrupt adds
            # "^C KeyboardInterrupt" as a user message, creating two user messages in a row
            assert coder.cur_messages[-1]["role"] == "assistant"

    async def test_normalize_language(self):
        coder = await Coder.create(self.GPT35, None, io=InputOutput())

        # Test None and empty
        assert coder.normalize_language(None) is None
        assert coder.normalize_language("") is None

        # Test "C" and "POSIX"
        assert coder.normalize_language("C") is None
        assert coder.normalize_language("POSIX") is None

        # Test already formatted names
        assert coder.normalize_language("English") == "English"
        assert coder.normalize_language("French") == "French"

        # Test common locale codes (fallback map, assuming babel is not installed or fails)
        with patch("cecli.coders.base_coder.Locale", None):
            assert coder.normalize_language("en_US") == "English"
            assert coder.normalize_language("fr_FR") == "French"
            assert coder.normalize_language("es") == "Spanish"
            assert coder.normalize_language("de_DE.UTF-8") == "German"
            assert coder.normalize_language("zh-CN") == "Chinese"
            # Test hyphen in fallback
            assert coder.normalize_language("ja") == "Japanese"
            assert coder.normalize_language("unknown_code") == "unknown_code"
            # Fallback to original

        # Test with babel.Locale mocked (available)
        mock_babel_locale = MagicMock()
        mock_locale_instance = MagicMock()
        mock_babel_locale.parse.return_value = mock_locale_instance

        with patch("cecli.coders.base_coder.Locale", mock_babel_locale):
            mock_locale_instance.get_display_name.return_value = "english"  # For en_US
            assert coder.normalize_language("en_US") == "English"
            mock_babel_locale.parse.assert_called_with("en_US")
            mock_locale_instance.get_display_name.assert_called_with("en")

            mock_locale_instance.get_display_name.return_value = "french"  # For fr-FR
            assert coder.normalize_language("fr-FR") == "French"  # Test with hyphen
            mock_babel_locale.parse.assert_called_with("fr_FR")  # Hyphen replaced
            mock_locale_instance.get_display_name.assert_called_with("en")

        # Test with babel.Locale raising an exception (simulating parse failure)
        mock_babel_locale_error = MagicMock()
        mock_babel_locale_error.parse.side_effect = Exception("Babel parse error")
        with patch("cecli.coders.base_coder.Locale", mock_babel_locale_error):
            assert coder.normalize_language("en_US") == "English"  # Falls back to map

    async def test_get_user_language(self):
        io = InputOutput()
        coder = await Coder.create(self.GPT35, None, io=io)

        # 1. Test with self.chat_language set
        coder.chat_language = "fr_CA"
        with patch.object(coder, "normalize_language", return_value="French Canadian") as mock_norm:
            assert coder.get_user_language() == "French Canadian"
            mock_norm.assert_called_once_with("fr_CA")
        coder.chat_language = None  # Reset

        # 2. Test with locale.getlocale()
        with patch("locale.getlocale", return_value=("en_GB", "UTF-8")) as mock_getlocale:
            with patch.object(
                coder, "normalize_language", return_value="British English"
            ) as mock_norm:
                assert coder.get_user_language() == "British English"
                mock_getlocale.assert_called_once()
                mock_norm.assert_called_once_with("en_GB")

        # Test with locale.getlocale() returning None or empty
        with patch("locale.getlocale", return_value=(None, None)) as mock_getlocale:
            with patch("os.environ.get") as mock_env_get:  # Ensure env vars are not used yet
                mock_env_get.return_value = None
                # Should be None if nothing found
                assert coder.get_user_language() is None

        # 3. Test with environment variables: LANG
        with patch(
            "locale.getlocale", side_effect=Exception("locale error")
        ):  # Mock locale to fail
            with patch("os.environ.get") as mock_env_get:
                mock_env_get.side_effect = lambda key: "de_DE.UTF-8" if key == "LANG" else None
                with patch.object(coder, "normalize_language", return_value="German") as mock_norm:
                    assert coder.get_user_language() == "German"
                    mock_env_get.assert_any_call("LANG")
                    mock_norm.assert_called_once_with("de_DE")

        # Test LANGUAGE (takes precedence over LANG if both were hypothetically checked
        # by os.environ.get, but our code checks in order, so we mock the first one it finds)
        with patch("locale.getlocale", side_effect=Exception("locale error")):
            with patch("os.environ.get") as mock_env_get:
                mock_env_get.side_effect = lambda key: "es_ES" if key == "LANGUAGE" else None
                with patch.object(coder, "normalize_language", return_value="Spanish") as mock_norm:
                    assert coder.get_user_language() == "Spanish"
                    # LANG would be called first
                    mock_env_get.assert_any_call("LANGUAGE")
                    mock_norm.assert_called_once_with("es_ES")

        # 4. Test priority: chat_language > locale > env
        coder.chat_language = "it_IT"
        with patch("locale.getlocale", return_value=("en_US", "UTF-8")) as mock_getlocale:
            with patch("os.environ.get", return_value="de_DE") as mock_env_get:
                with patch.object(
                    coder, "normalize_language", side_effect=lambda x: x.upper()
                ) as mock_norm:
                    assert coder.get_user_language() == "IT_IT"  # From chat_language
                    mock_norm.assert_called_once_with("it_IT")
                    mock_getlocale.assert_not_called()
                    mock_env_get.assert_not_called()
        coder.chat_language = None

        # 5. Test when no language is found
        with patch("locale.getlocale", side_effect=Exception("locale error")):
            with patch("os.environ.get", return_value=None) as mock_env_get:
                assert coder.get_user_language() is None

    async def test_architect_coder_auto_accept_true(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            io.confirm_ask = AsyncMock(return_value=False)

            coder = await Coder.create(self.GPT35, edit_format="architect", io=io)
            coder.auto_accept_architect = True
            coder.partial_response_content = "Make these changes to the code"

            mock_editor = MagicMock()
            mock_editor.generate = AsyncMock()
            mock_editor.total_cost = 0
            mock_editor.coder_commit_hashes = []

            with patch(
                "cecli.coders.architect_coder.Coder.create",
                new_callable=AsyncMock,
                return_value=mock_editor,
            ):
                with pytest.raises(SwitchCoderSignal):
                    await coder.reply_completed()

                io.confirm_ask.assert_called_once_with("Edit the files?", allow_tweak=False)
                mock_editor.generate.assert_called_once()

    async def test_architect_coder_auto_accept_false_confirmed(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=False)
            io.confirm_ask = AsyncMock(return_value=True)

            coder = await Coder.create(self.GPT35, edit_format="architect", io=io)
            coder.auto_accept_architect = False
            coder.partial_response_content = "Make these changes to the code"

            mock_editor = MagicMock()
            mock_editor.generate = AsyncMock()
            mock_editor.total_cost = 0
            mock_editor.coder_commit_hashes = []

            with patch(
                "cecli.coders.architect_coder.Coder.create",
                new_callable=AsyncMock,
                return_value=mock_editor,
            ):
                with pytest.raises(SwitchCoderSignal):
                    await coder.reply_completed()

                io.confirm_ask.assert_called_once_with("Edit the files?", allow_tweak=False)
                mock_editor.generate.assert_called_once()

    async def test_architect_coder_auto_accept_false_rejected(self):
        with GitTemporaryDirectory():
            io = InputOutput(yes=False)
            io.confirm_ask = AsyncMock(return_value=False)

            coder = await Coder.create(self.GPT35, edit_format="architect", io=io)
            coder.auto_accept_architect = False
            coder.partial_response_content = "Make these changes to the code"

            mock_create = AsyncMock()
            with patch(
                "cecli.coders.architect_coder.Coder.create",
                mock_create,
            ):
                result = await coder.reply_completed()

                assert result is None
                io.confirm_ask.assert_called_once_with("Edit the files?", allow_tweak=False)
                mock_create.assert_not_called()

    async def test_process_tool_calls_none_response(self):
        """Test that process_tool_calls handles None response correctly."""
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Test with None response
            result = await coder.process_tool_calls(None)
            assert not result

    async def test_process_tool_calls_no_tool_calls(self):
        """Test that process_tool_calls handles response with no tool calls."""
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Create a response with no tool calls
            response = MagicMock()
            response.choices = [MagicMock()]
            response.choices[0].message = MagicMock()
            response.choices[0].message.tool_calls = []

            result = await coder.process_tool_calls(response)
            assert not result

    async def test_process_tool_calls_with_tools(self):
        """Test that process_tool_calls processes tool calls correctly."""
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            io.confirm_ask = AsyncMock(return_value=True)

            # Create mock MCP server
            mock_server = MagicMock()
            mock_server.name = "test_server"
            mock_server.connect = AsyncMock()
            mock_server.disconnect = AsyncMock()

            manager = McpServerManager([mock_server])
            manager._connected_servers = [mock_server]

            # Create a tool call
            tool_call = MagicMock()
            tool_call.id = "test_id"
            tool_call.type = "function"
            tool_call.function = MagicMock()
            tool_call.function.name = "test_tool"
            tool_call.function.arguments = '{"param": "value"}'

            # Create a response with tool calls
            response = MagicMock()
            response.choices = [MagicMock()]
            response.choices[0].message = MagicMock()
            response.choices[0].message.tool_calls = [tool_call]
            response.choices[0].message.to_dict = MagicMock(
                return_value={"role": "assistant", "tool_calls": [{"id": "test_id"}]}
            )

            # Create coder with mock MCP tools and servers
            manager._server_tools[mock_server.name] = [{"function": {"name": "test_tool"}}]
            coder = await Coder.create(self.GPT35, "diff", io=io, mcp_manager=manager)

            # Mock _execute_tool_calls to return tool responses
            tool_responses = [
                {
                    "role": "tool",
                    "tool_call_id": "test_id",
                    "content": "Tool execution result",
                }
            ]
            coder._execute_tool_calls = AsyncMock(return_value=tool_responses)

            # Test process_tool_calls
            result = await coder.process_tool_calls(response)
            assert result

            # Verify that _execute_tool_calls was called
            coder._execute_tool_calls.assert_called_once()

            # Verify that the tool response message was added
            assert len(coder.cur_messages) == 1
            assert coder.cur_messages[0]["role"] == "tool"
            assert coder.cur_messages[0]["tool_call_id"] == "test_id"
            assert coder.cur_messages[0]["content"] == "Tool execution result"

    async def test_process_tool_calls_max_calls_exceeded(self):
        """Test that process_tool_calls handles max tool calls exceeded."""
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            io.tool_warning = MagicMock()

            # Create a tool call
            tool_call = MagicMock()
            tool_call.id = "test_id"
            tool_call.type = "function"
            tool_call.function = MagicMock()
            tool_call.function.name = "test_tool"

            # Create a response with tool calls
            response = MagicMock()
            response.choices = [MagicMock()]
            response.choices[0].message = MagicMock()
            response.choices[0].message.tool_calls = [tool_call]

            # Create mock MCP server
            mock_server = MagicMock()
            mock_server.name = "test_server"
            mock_server.connect = AsyncMock()
            mock_server.session = AsyncMock()

            manager = McpServerManager([mock_server])
            manager._connected_servers = [mock_server]

            # Create coder with max tool calls exceeded
            manager._server_tools[mock_server.name] = [{"function": {"name": "test_tool"}}]
            coder = await Coder.create(self.GPT35, "diff", io=io, mcp_manager=manager)
            coder.num_tool_calls = coder.max_tool_calls

            # Test process_tool_calls
            result = await coder.process_tool_calls(response)
            assert not result

            # Verify that warning was shown
            io.tool_warning.assert_called_once_with(
                f"Only {coder.max_tool_calls} tool calls allowed, stopping."
            )

    async def test_process_tool_calls_user_rejects(self):
        """Test that process_tool_calls handles user rejection."""
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            io.confirm_ask = AsyncMock(return_value=False)

            # Create a tool call
            tool_call = MagicMock()
            tool_call.id = "test_id"
            tool_call.type = "function"
            tool_call.function = MagicMock()
            tool_call.function.name = "test_tool"

            # Create a response with tool calls
            response = MagicMock()
            response.choices = [MagicMock()]
            response.choices[0].message = MagicMock()
            response.choices[0].message.tool_calls = [tool_call]

            # Create mock MCP server
            mock_server = MagicMock()
            mock_server.name = "test_server"
            mock_server.connect = AsyncMock()
            mock_server.disconnect = AsyncMock()

            manager = McpServerManager([mock_server])
            manager._connected_servers = [mock_server]

            # Create coder with mock MCP tools
            manager._server_tools[mock_server.name] = [{"function": {"name": "test_tool"}}]
            coder = await Coder.create(self.GPT35, "diff", io=io, mcp_manager=manager)

            # Test process_tool_calls
            result = await coder.process_tool_calls(response)
            assert not result

            # Verify that confirm_ask was called
            io.confirm_ask.assert_called_once_with("Run tools?", group_response="Run MCP Tools")

            # Verify that no messages were added
            assert len(coder.cur_messages) == 0

    @patch(
        "cecli.coders.base_coder.experimental_mcp_client.call_openai_tool", new_callable=AsyncMock
    )
    async def test_execute_tool_calls(self, mock_call_tool):
        """Test that _execute_tool_calls executes tool calls correctly."""
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Create mock server and tool call
            mock_server = MagicMock()
            mock_server.name = "test_server"
            mock_server.connect = AsyncMock(return_value=MagicMock())
            mock_server.disconnect = AsyncMock()

            tool_call = MagicMock()
            tool_call.id = "test_id"
            tool_call.type = "function"
            tool_call.function = MagicMock()
            tool_call.function.name = "test_tool"
            tool_call.function.arguments = '{"param": "value"}'

            # Create server_tool_calls
            server_tool_calls = {mock_server: [tool_call]}

            # Mock call_openai_tool to return a result with content
            mock_content_item = MagicMock(spec=["text"])
            mock_content_item.text = "Tool execution result"

            mock_result = MagicMock(spec=["content"])
            mock_result.content = [mock_content_item]
            mock_call_tool.return_value = mock_result

            # Test _execute_tool_calls directly
            result = await coder._execute_tool_calls(server_tool_calls)

            # Verify that server.connect was called
            mock_server.connect.assert_called_once()

            # Verify that the correct tool responses were returned
            assert len(result) == 1
            assert result[0]["role"] == "tool"
            assert result[0]["tool_call_id"] == "test_id"
            assert result[0]["content"] == "Tool execution result"

    async def test_auto_commit_with_none_content_message(self):
        """
        Verify that auto_commit works with messages that have None content.
        This is common with tool calls.
        """
        with GitTemporaryDirectory():
            repo = git.Repo()

            fname = Path("file1.txt")
            fname.write_text("one\n")
            repo.git.add(str(fname))
            repo.git.commit("-m", "initial")

            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])

            # Clear any existing messages and add test messages to ConversationManager
            cur_messages = [
                {"role": "user", "content": "do a thing"},
                {"role": "assistant", "content": None},
            ]

            # Add messages to ConversationManager
            for msg in cur_messages:
                ConversationManager.add_message(msg, MessageTag.CUR)

            # The context for commit message will be generated from cur_messages.
            # This call should not raise an exception due to `content: None`.

            async def mock_get_commit_message(diffs, context, user_language=None):
                assert "USER: do a thing" in context
                # None becomes empty string.
                assert "ASSISTANT: \n" in context
                return "commit message"

            coder.repo.get_commit_message = AsyncMock(side_effect=mock_get_commit_message)

            # To trigger a commit, the file must be modified
            fname.write_text("one changed\n")

            res = await coder.auto_commit({str(fname)})
            assert res is not None

            # A new commit should be created
            num_commits = len(list(repo.iter_commits()))
            assert num_commits == 2

            coder.repo.get_commit_message.assert_called_once()

    @patch(
        "cecli.coders.base_coder.experimental_mcp_client.call_openai_tool",
        new_callable=AsyncMock,
    )
    async def test_execute_tool_calls_multiple_content(self, mock_call_openai_tool):
        """Test that _execute_tool_calls handles multiple content blocks correctly."""
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Create mock server and tool call
            mock_server = AsyncMock()
            mock_server.name = "test_server"

            tool_call = MagicMock()
            tool_call.id = "test_id"
            tool_call.type = "function"
            tool_call.function = MagicMock()
            tool_call.function.name = "test_tool"
            tool_call.function.arguments = '{"param": "value"}'

            # Create server_tool_calls
            server_tool_calls = {mock_server: [tool_call]}

            # Mock the return value of call_openai_tool
            mock_content1 = MagicMock(spec=["text"])
            mock_content1.text = "First part. "
            mock_content2 = MagicMock(spec=["text"])
            mock_content2.text = "Second part."

            mock_call_result = MagicMock()
            mock_call_result.content = [mock_content1, mock_content2]
            mock_call_openai_tool.return_value = mock_call_result

            # Test _execute_tool_calls directly
            result = await coder._execute_tool_calls(server_tool_calls)

            # Verify that call_openai_tool was called
            mock_call_openai_tool.assert_called_once()

            # Verify that the correct tool responses were returned
            assert len(result) == 1
            assert result[0]["role"] == "tool"
            assert result[0]["tool_call_id"] == "test_id"
            # This will fail with the current code, which is the point of the test.
            # The current code returns a hardcoded string.
            # A fixed version should concatenate the text from all content blocks.
            assert result[0]["content"] == "First part. Second part."

    @patch(
        "cecli.coders.base_coder.experimental_mcp_client.call_openai_tool",
        new_callable=AsyncMock,
    )
    async def test_execute_tool_calls_blob_content(self, mock_call_openai_tool):
        """Test that _execute_tool_calls handles BlobResourceContents correctly."""
        with GitTemporaryDirectory():
            io = InputOutput(yes=True)
            coder = await Coder.create(self.GPT35, "diff", io=io)

            # Create mock server and tool call
            mock_server = AsyncMock()
            mock_server.name = "test_server"

            tool_call = MagicMock()
            tool_call.id = "test_id"
            tool_call.type = "function"
            tool_call.function = MagicMock()
            tool_call.function.name = "test_tool"
            tool_call.function.arguments = '{"param": "value"}'

            # Create server_tool_calls
            server_tool_calls = {mock_server: [tool_call]}

            # Mock BlobResourceContents for text
            text_blob_content = "Hello from blob! "
            encoded_text_blob = base64.b64encode(text_blob_content.encode("utf-8")).decode("utf-8")
            mock_text_blob_resource = MagicMock(spec=["blob"])
            mock_text_blob_resource.blob = encoded_text_blob

            mock_embedded_text_resource = MagicMock(spec=["resource"])
            mock_embedded_text_resource.resource = mock_text_blob_resource

            # Mock BlobResourceContents for binary data
            binary_blob_content = b"\x80\x81\x82"
            encoded_binary_blob = base64.b64encode(binary_blob_content).decode("utf-8")
            mock_binary_blob_resource = MagicMock(spec=["blob", "name", "mimeType"])
            mock_binary_blob_resource.blob = encoded_binary_blob
            mock_binary_blob_resource.name = "binary.dat"
            mock_binary_blob_resource.mimeType = "application/octet-stream"

            mock_embedded_binary_resource = MagicMock(spec=["resource"])
            mock_embedded_binary_resource.resource = mock_binary_blob_resource

            # Mock TextContent
            mock_text_content = MagicMock(spec=["text"])
            mock_text_content.text = "Plain text. "

            mock_call_result = MagicMock()
            mock_call_result.content = [
                mock_text_content,
                mock_embedded_text_resource,
                mock_embedded_binary_resource,
            ]
            mock_call_openai_tool.return_value = mock_call_result

            # Test _execute_tool_calls directly
            result = await coder._execute_tool_calls(server_tool_calls)

            # Verify that call_openai_tool was called
            mock_call_openai_tool.assert_called_once()

            # Verify that the correct tool responses were returned
            assert len(result) == 1
            assert result[0]["role"] == "tool"
            assert result[0]["tool_call_id"] == "test_id"

            expected_content = (
                "Plain text. Hello from blob! [embedded binary resource: binary.dat"
                " (application/octet-stream)]"
            )
            assert result[0]["content"] == expected_content
