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

import pytest

from cecli.coders import Coder
from cecli.coders.wholefile_coder import WholeFileCoder
from cecli.dump import dump  # noqa: F401
from cecli.helpers.conversation import ConversationChunks
from cecli.io import InputOutput


class TestWholeFileCoder:
    @pytest.fixture(autouse=True)
    def setup_and_teardown(self, gpt35_model):
        self.original_cwd = os.getcwd()
        self.tempdir = tempfile.mkdtemp()
        os.chdir(self.tempdir)
        self.GPT35 = gpt35_model
        # Reset conversation system before each test
        ConversationChunks.reset()

        yield

        os.chdir(self.original_cwd)
        shutil.rmtree(self.tempdir, ignore_errors=True)
        # Reset conversation system after each test
        ConversationChunks.reset()

    async def test_no_files(self):
        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)

        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[])
        coder.partial_response_content = (
            'To print "Hello, World!" in most programming languages, you can use the following'
            ' code:\n\n```python\nprint("Hello, World!")\n```\n\nThis code will output "Hello,'
            ' World!" to the console.'
        )

        # This is throwing ValueError!
        coder.render_incremental_response(True)

    async def test_no_files_new_file_should_ask(self):
        io = InputOutput(yes=False)
        io.confirm_ask = AsyncMock(return_value=False)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[])
        coder.partial_response_content = (
            'To print "Hello, World!" in most programming languages, you can use the following'
            ' code:\n\nfoo.js\n```python\nprint("Hello, World!")\n```\n\nThis code will output'
            ' "Hello, World!" to the console.'
        )
        await coder.apply_updates()
        assert not Path("foo.js").exists()

    async def test_update_files(self):
        # Create a sample file in the temporary directory
        sample_file = "sample.txt"
        with open(sample_file, "w") as f:
            f.write("Original content\n")

        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])

        # Set the partial response content with the updated content
        coder.partial_response_content = f"{sample_file}\n```\nUpdated content\n```"

        # Call update_files method
        edited_files = await coder.apply_updates()

        # Check if the sample file was updated
        assert "sample.txt" in edited_files

        # Check if the content of the sample file was updated
        with open(sample_file, "r") as f:
            updated_content = f.read()
        assert updated_content == "Updated content\n"

    async def test_update_files_live_diff(self):
        # Create a sample file in the temporary directory
        sample_file = "sample.txt"
        with open(sample_file, "w") as f:
            f.write("\n".join(map(str, range(0, 100))))

        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])

        # Set the partial response content with the updated content
        coder.partial_response_content = f"{sample_file}\n```\n0\n\1\n2\n"

        lines = coder.get_edits(mode="diff").splitlines()

        # the live diff should be concise, since we haven't changed anything yet
        assert len(lines) < 20

    async def test_update_files_with_existing_fence(self):
        # Create a sample file in the temporary directory
        sample_file = "sample.txt"
        original_content = """
Here is some quoted text:
```
Quote!
```
"""
        with open(sample_file, "w") as f:
            f.write(original_content)

        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])

        coder.choose_fence()

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

        # Set the partial response content with the updated content
        coder.partial_response_content = (
            f"{sample_file}\n{coder.fence[0]}\nUpdated content\n{coder.fence[1]}"
        )

        # Call update_files method
        edited_files = await coder.apply_updates()

        # Check if the sample file was updated
        assert "sample.txt" in edited_files

        # Check if the content of the sample file was updated
        with open(sample_file, "r") as f:
            updated_content = f.read()
        assert updated_content == "Updated content\n"

    async def test_update_files_bogus_path_prefix(self):
        # Create a sample file in the temporary directory
        sample_file = "sample.txt"
        with open(sample_file, "w") as f:
            f.write("Original content\n")

        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])

        # Set the partial response content with the updated content
        # With path/to/ prepended onto the filename
        coder.partial_response_content = f"path/to/{sample_file}\n```\nUpdated content\n```"

        # Call update_files method
        edited_files = await coder.apply_updates()

        # Check if the sample file was updated
        assert "sample.txt" in edited_files

        # Check if the content of the sample file was updated
        with open(sample_file, "r") as f:
            updated_content = f.read()
        assert updated_content == "Updated content\n"

    async def test_update_files_not_in_chat(self):
        # Create a sample file in the temporary directory
        sample_file = "sample.txt"
        with open(sample_file, "w") as f:
            f.write("Original content\n")

        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io)

        # Set the partial response content with the updated content
        coder.partial_response_content = f"{sample_file}\n```\nUpdated content\n```"

        # Call update_files method
        edited_files = await coder.apply_updates()

        # Check if the sample file was updated
        assert "sample.txt" in edited_files

        # Check if the content of the sample file was updated
        with open(sample_file, "r") as f:
            updated_content = f.read()
        assert updated_content == "Updated content\n"

    async def test_update_files_no_filename_single_file_in_chat(self):
        sample_file = "accumulate.py"
        content = (
            "def accumulate(collection, operation):\n    return [operation(x) for x in"
            " collection]\n"
        )

        with open(sample_file, "w") as f:
            f.write("Original content\n")

        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])

        # Set the partial response content with the updated content
        coder.partial_response_content = (
            f"Here's the modified `{sample_file}` file that implements the `accumulate`"
            f" function as per the given instructions:\n\n```\n{content}```\n\nThis"
            " implementation uses a list comprehension to apply the `operation` function to"
            " each element of the `collection` and returns the resulting list."
        )

        # Call update_files method
        edited_files = await coder.apply_updates()

        # Check if the sample file was updated
        assert sample_file in edited_files

        # Check if the content of the sample file was updated
        with open(sample_file, "r") as f:
            updated_content = f.read()
        assert updated_content == content

    async def test_update_files_earlier_filename(self):
        fname_a = Path("a.txt")
        fname_b = Path("b.txt")

        fname_a.write_text("before a\n")
        fname_b.write_text("before b\n")

        response = """
Here is a new version of `a.txt` for you to consider:

```
after a
```

And here is `b.txt`:

```
after b
```
"""
        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[fname_a, fname_b])

        # Set the partial response content with the updated content
        coder.partial_response_content = response

        # Call update_files method
        edited_files = await coder.apply_updates()

        # Check if the sample file was updated
        assert str(fname_a) in edited_files
        assert str(fname_b) in edited_files

        assert fname_a.read_text() == "after a\n"
        assert fname_b.read_text() == "after b\n"

    async def test_update_hash_filename(self):
        fname_a = Path("a.txt")
        fname_b = Path("b.txt")

        fname_a.write_text("before a\n")
        fname_b.write_text("before b\n")

        response = """

### a.txt
```
after a
```

### b.txt
```
after b
```
"""
        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[fname_a, fname_b])

        # Set the partial response content with the updated content
        coder.partial_response_content = response

        # Call update_files method
        edited_files = await coder.apply_updates()

        dump(edited_files)

        # Check if the sample file was updated
        assert str(fname_a) in edited_files
        assert str(fname_b) in edited_files

        assert fname_a.read_text() == "after a\n"
        assert fname_b.read_text() == "after b\n"

    async def test_update_named_file_but_extra_unnamed_code_block(self):
        sample_file = "hello.py"
        new_content = "new\ncontent\ngoes\nhere\n"

        with open(sample_file, "w") as f:
            f.write("Original content\n")

        # Initialize WholeFileCoder with the temporary directory
        io = InputOutput(yes=True)
        coder = WholeFileCoder(main_model=self.GPT35, io=io, fnames=[sample_file])

        # Set the partial response content with the updated content
        coder.partial_response_content = (
            f"Here's the modified `{sample_file}` file that implements the `accumulate`"
            f" function as per the given instructions:\n\n```\n{new_content}```\n\nThis"
            " implementation uses a list comprehension to apply the `operation` function to"
            " each element of the `collection` and returns the resulting list.\n"
            "Run it like this:\n\n"
            "```\npython {sample_file}\n```\n\n"
        )

        # Call update_files method
        edited_files = await coder.apply_updates()

        # Check if the sample file was updated
        assert sample_file in edited_files

        # Check if the content of the sample file was updated
        with open(sample_file, "r") as f:
            updated_content = f.read()
        assert updated_content == new_content

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

        with open(file1, "w", encoding="utf-8") as f:
            f.write("one\ntwo\nthree\n")

        files = [file1]

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

        # no trailing newline so the response content below doesn't add ANOTHER newline
        new_content = "new\ntwo\nthree"

        async def mock_send(*args, **kwargs):
            content = f"""
Do this:

{Path(file1).name}
```
{new_content}
```

"""
            coder.partial_response_content = content
            coder.partial_response_function_call = dict()

            # Create a mock response object that looks like a LiteLLM response
            mock_response = MagicMock()
            # Mock model_dump() to return the expected structure
            mock_response.model_dump = lambda: {
                "choices": [{"message": {"content": content, "role": "assistant"}}]
            }
            # Also mock __getitem__ for backward compatibility
            mock_response.__getitem__ = lambda self, key: (
                [{"message": {"content": content, "role": "assistant"}}] if key == "choices" else {}
            )

            coder.partial_response_chunks = [mock_response]
            # Make this an async generator by using return (stops iteration immediately)
            return
            yield  # This line makes it an async generator, but is never reached

        coder.send = mock_send

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

        content = Path(file1).read_text(encoding="utf-8")

        # check for one trailing newline
        assert content == new_content + "\n"
