failing test
```
_________________________________________________ test_non_file_related_messages_preserved __________________________________________________

mock_app_state = <test_conversation_history.mock_app_state.<locals>.MockAppState object at 0x111509040>
mock_file_context = <test_conversation_history.mock_file_context.<locals>.MockFileContextTracker object at 0x111509010>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x111521fa0>

    async def test_non_file_related_messages_preserved(mock_app_state, mock_file_context, monkeypatch):
        """
        Test that non-file-related messages are preserved correctly when file operations occur.
        Ensures that updating a file does not interfere with unrelated conversation history.
        """
    
        # 1. Initial Setup: Reset conversation_history and tracked_files
        mock_app_state.conversation_history = [
            {'role': 'user', 'content': 'Hello'},
            {'role': 'assistant', 'content': 'Hi there!'},
            {'role': 'user', 'content': "What's the weather like?"},
            {'role': 'assistant', 'content': "I'm sorry, I don't have real-time weather information."},
        ]
        mock_file_context.tracked_files = {}
    
        # 2. Define the user input for updating a file
        user_input = "Update file1.txt"
    
        # 3. Mock AI client response for file1.txt update
        # Assistant should acknowledge the update and provide a diff
        mock_response = MockResponse(
            usage={
                'input_tokens': 50,
                'output_tokens': 30,
                'total_tokens': 80,
                'cache_creation_input_tokens': 0,
                'cache_read_input_tokens': 0
            },
            response_text=(
                "I've updated `file1.txt` to the context.\n"
                "### Diff for file1.txt\n"
                "```diff\n"
                "@@ -1 +1 @@\n"
                "-This is the content of file1\n"
                "+Updated content of file1\n"
                "```"
            )
        )
    
        ai_client_mock = AsyncMock()
        ai_client_mock.create_message.return_value = mock_response
        ai_client_mock.extract_function_calls = AsyncMock(return_value=[])  # No function calls in this test
    
        # 4. Setup AIClientFactory.get_ai_client to return the mock
        monkeypatch.setattr(
            "chat.chat.AIClientFactory.get_ai_client",
            lambda x: ai_client_mock
        )
    
        # 5. Monkeypatch get_app_state and get_file_context to return the mocks
        monkeypatch.setattr("chat.chat.get_app_state", lambda: mock_app_state)
        monkeypatch.setattr("chat.chat.get_file_context", lambda: mock_file_context)
    
        # 6. Mock the add_or_update_file method to correctly set the content
        def mock_add_or_update_file(path, content, operation):
            mock_file_context.tracked_files[path] = content
    
        mock_file_context.add_or_update_file = MagicMock(side_effect=mock_add_or_update_file)
    
        # 6.a. Mock the get_file_content method to return the current tracked content
        mock_file_context.get_file_content = MagicMock(side_effect=lambda path: mock_file_context.tracked_files.get(path))
    
        # 7. Simulate User Interaction: Update file1.txt
>       assistant_response = await chat_with_model(user_input, MagicMock(), "gpt-4")

tests/test_chat/test_conversation_history.py:728: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
chat/chat.py:324: in chat_with_model
    assistant_response = await process_file_updates(assistant_response, file_context, app_state)
chat/chat.py:342: in process_file_updates
    updated_files = detect_file_updates(assistant_response)
chat/chat.py:391: in detect_file_updates
    for match in re.finditer(pattern_added, assistant_message):
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

pattern = 'Added\\s+`?(?P<path>[^`]+)`?\\s+to context\\.', string = <AsyncMock name='mock.extract_response()' id='4585148816'>, flags = 0

    def finditer(pattern, string, flags=0):
        """Return an iterator over all non-overlapping matches in the
        string.  For each match, the iterator returns a Match object.
    
        Empty matches are included in the result."""
>       return _compile(pattern, flags).finditer(string)
E       TypeError: expected string or bytes-like object, got 'AsyncMock'

/usr/local/Cellar/python@3.12/3.12.4/Frameworks/Python.framework/Versions/3.12/lib/python3.12/re/__init__.py:224: TypeError
----------------------------------------------------------- Captured stdout setup -----------------------------------------------------------
DEBUG: Using selector: KqueueSelector
------------------------------------------------------------ Captured log setup -------------------------------------------------------------
DEBUG    asyncio:selector_events.py:64 Using selector: KqueueSelector
----------------------------------------------------------- Captured stdout call ------------------------------------------------------------
INFO: Processing user input: Update file1.txt
INFO: Using model: gpt-4
INFO: Available tools: []
INFO: Using AI client: AsyncMock
INFO: context awareness prompt: No files in context.
INFO: History passed to AI client: [{'role': 'user', 'content': 'Hello'}, {'role': 'assistant', 'content': 'Hi there!'}, {'role': 'user', 'content': "What's the weather like?"}, {'role': 'assistant', 'content': "I'm sorry, I don't have real-time weather information."}]
INFO: Creating message with AI client
INFO: Message created successfully
WARNING: Non-integer value encountered for prompt_tokens in token usage update for gpt-4. Using default value 0.
WARNING: Non-integer value encountered for completion_tokens in token usage update for gpt-4. Using default value 0.
INFO: EXTRACTED RESPONSE: <AsyncMock name='mock.extract_response()' id='4585148816'>
------------------------------------------------------------- Captured log call -------------------------------------------------------------
INFO     root:chat.py:259 Processing user input: Update file1.txt
INFO     root:chat.py:260 Using model: gpt-4
INFO     root:chat.py:261 Available tools: []
INFO     root:chat.py:271 Using AI client: AsyncMock
INFO     root:chat.py:277 context awareness prompt: No files in context.
INFO     root:chat.py:281 History passed to AI client: [{'role': 'user', 'content': 'Hello'}, {'role': 'assistant', 'content': 'Hi there!'}, {'role': 'user', 'content': "What's the weather like?"}, {'role': 'assistant', 'content': "I'm sorry, I don't have real-time weather information."}]
INFO     root:chat.py:284 Creating message with AI client
INFO     root:chat.py:291 Message created successfully
WARNING  root:token_usage.py:127 Non-integer value encountered for prompt_tokens in token usage update for gpt-4. Using default value 0.
WARNING  root:token_usage.py:127 Non-integer value encountered for completion_tokens in token usage update for gpt-4. Using default value 0.
INFO     root:chat.py:321 EXTRACTED RESPONSE: <AsyncMock name='mock.extract_response()' id='4585148816'>
--------------------------------------------------------- Captured stdout teardown ----------------------------------------------------------
DEBUG: Using selector: KqueueSelector
----------------------------------------------------------- Captured log teardown -----------------------------------------------------------
DEBUG    asyncio:selector_events.py:64 Using selector: KqueueSelector
========================================================== short test summary info ==========================================================
FAILED tests/test_chat/test_conversation_history.py::test_non_file_related_messages_preserved - TypeError: expected string or bytes-like object, got 'AsyncMock'
======================
```

test not set up right
```
# Define the MockResponse class
class MockResponse:
    def __init__(self, usage, response_text):
        self.usage = MagicMock(**usage) if usage else None
        self.extract_response = AsyncMock(return_value=response_text)
        self.content = []  # Adjust based on actual response structure

async def test_non_file_related_messages_preserved(mock_app_state, mock_file_context, monkeypatch):
    """
    Test that non-file-related messages are preserved correctly when file operations occur.
    Ensures that updating a file does not interfere with unrelated conversation history.
    """
    
    # 1. Initial Setup: Reset conversation_history and tracked_files
    mock_app_state.conversation_history = [
        {'role': 'user', 'content': 'Hello'},
        {'role': 'assistant', 'content': 'Hi there!'},
        {'role': 'user', 'content': "What's the weather like?"},
        {'role': 'assistant', 'content': "I'm sorry, I don't have real-time weather information."},
    ]
    mock_file_context.tracked_files = {}
    
    # 2. Define the user input for updating a file
    user_input = "Update file1.txt"
    
    # 3. Mock AI client response for file1.txt update
    # Assistant should acknowledge the update and provide a diff
    mock_response = MockResponse(
        usage={
            'input_tokens': 50,
            'output_tokens': 30,
            'total_tokens': 80,
            'cache_creation_input_tokens': 0,
            'cache_read_input_tokens': 0
        },
        response_text=(
            "I've updated `file1.txt` to the context.\n"
            "### Diff for file1.txt\n"
            "```diff\n"
            "@@ -1 +1 @@\n"
            "-This is the content of file1\n"
            "+Updated content of file1\n"
            "```"
        )
    )
    
    ai_client_mock = AsyncMock()
    ai_client_mock.create_message.return_value = mock_response
    ai_client_mock.extract_function_calls = AsyncMock(return_value=[])  # No function calls in this test
    
    # 4. Setup AIClientFactory.get_ai_client to return the mock
    monkeypatch.setattr(
        "chat.chat.AIClientFactory.get_ai_client",
        lambda x: ai_client_mock
    )
    
    # 5. Monkeypatch get_app_state and get_file_context to return the mocks
    monkeypatch.setattr("chat.chat.get_app_state", lambda: mock_app_state)
    monkeypatch.setattr("chat.chat.get_file_context", lambda: mock_file_context)
    
    # 6. Mock the add_or_update_file method to correctly set the content
    def mock_add_or_update_file(path, content, operation):
        mock_file_context.tracked_files[path] = content
    
    mock_file_context.add_or_update_file = MagicMock(side_effect=mock_add_or_update_file)
    
    # 6.a. Mock the get_file_content method to return the current tracked content
    mock_file_context.get_file_content = MagicMock(side_effect=lambda path: mock_file_context.tracked_files.get(path))
    
    # 7. Simulate User Interaction: Update file1.txt
    assistant_response = await chat_with_model(user_input, MagicMock(), "gpt-4")
    
    # 8. Print internal states for debugging (optional)
    print("Conversation History:", mock_app_state.conversation_history)
    print("Tracked Files:", mock_file_context.tracked_files)
    
    # 9. Assertions
    
    # 9.1. Check that conversation_history has 6 messages (4 original + 2 new)
    expected_conversation_length = 6
    assert len(mock_app_state.conversation_history) == expected_conversation_length, \
        f"Expected {expected_conversation_length} messages in conversation_history, found {len(mock_app_state.conversation_history)}"
    
    # 9.2. Verify that the last two messages are related to the file update
    # The assistant should have appended the acknowledgment and the diff
    ack_message = "I've updated `file1.txt` to the context."
    diff_message = (
        "### Diff for file1.txt\n"
        "```diff\n"
        "@@ -1 +1 @@\n"
        "-This is the content of file1\n"
        "+Updated content of file1\n"
        "```"
    )
    
    assert mock_app_state.conversation_history[-2]['role'] == 'assistant', \
        f"Expected second last message to be from assistant, got {mock_app_state.conversation_history[-2]['role']}"
    assert ack_message in mock_app_state.conversation_history[-2]['content'], \
        f"Expected acknowledgment message not found in second last message: {mock_app_state.conversation_history[-2]['content']}"
    
    assert mock_app_state.conversation_history[-1]['role'] == 'assistant', \
        f"Expected last message to be from assistant, got {mock_app_state.conversation_history[-1]['role']}"
    assert diff_message in mock_app_state.conversation_history[-1]['content'], \
        f"Expected diff message not found in last message: {mock_app_state.conversation_history[-1]['content']}"
    
    # 9.3. Verify that non-file-related messages are preserved
    # Ensure that the original non-file messages remain unchanged
    original_messages = [
        {'role': 'user', 'content': 'Hello'},
        {'role': 'assistant', 'content': 'Hi there!'},
        {'role': 'user', 'content': "What's the weather like?"},
        {'role': 'assistant', 'content': "I'm sorry, I don't have real-time weather information."},
    ]
    
    for i, original_msg in enumerate(original_messages):
        assert mock_app_state.conversation_history[i] == original_msg, \
            f"Original message at index {i} was altered. Expected {original_msg}, got {mock_app_state.conversation_history[i]}"
    
    # 9.4. Verify FileContextTracker Updates
    # The file should now have the updated content
    assert "file1.txt" in mock_file_context.tracked_files, \
        f"Expected 'file1.txt' to be in tracked_files, got {list(mock_file_context.tracked_files.keys())}"
    assert mock_file_context.tracked_files["file1.txt"] == "Updated content of file1", \
        f"Expected tracked_files['file1.txt'] to be 'Updated content of file1', got '{mock_file_context.tracked_files.get('file1.txt')}'"
    
    # 9.5. Ensure No Unexpected Files are Tracked
    assert len(mock_file_context.tracked_files) == 1, \
        f"Expected only 'file1.txt' in tracked_files, got {list(mock_file_context.tracked_files.keys())}"
    
    # 10. Ensure the assistant_response reflects the latest changes appropriately
    expected_assistant_response = f"{ack_message}\n{diff_message}"
    assert assistant_response == expected_assistant_response, \
        f"Expected assistant_response to be '{expected_assistant_response}', got '{assistant_response}'"
```

chat.py
```
import logging
import json
import os
import re
import difflib
from typing import List, Dict, Any, Tuple, Optional
from efc.efc_cache import get_file_context
from app_state import get_app_state
from chat.model_config import get_model_config
from tools.tools import ToolManager
from utils.token_usage import update_token_usage
from chat.factory import AIClientFactory
import asyncio

# Set logging level
# logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.DEBUG)


async def chat_with_model(user_input: str, tool_manager: ToolManager, model_name: str) -> str:
    logging.info(f"Processing user input: {user_input}")
    logging.info(f"Using model: {model_name}")
    logging.info(f"Available tools: {[tool['function']['name'] for tool in tool_manager.get_tool_descriptions()]}")

    app_state = get_app_state()
    # Add the new user input to the history
    app_state.conversation_history.append({"role": "user", "content": user_input})

    file_context = get_file_context()
    model_config = get_model_config(model_name)

    ai_client = AIClientFactory.get_ai_client(model_name)
    logging.info(f"Using AI client: {type(ai_client).__name__}")

    optimized_history = app_state.conversation_history[:-1]  # Exclude the latest user message

    # System message includes context awareness prompt
    file_context_summary = file_context.generate_context_awareness_prompt(app_state)
    logging.info(f"context awareness prompt: {file_context_summary}")
    system_message = f"{model_config['base_system_prompt']}\n\n{file_context_summary}"

    tools = tool_manager.get_tool_descriptions(format="claude" if model_name.lower() in ['sonnet', 'claude-3-sonnet-20240229'] else "default")
    logging.info(f"History passed to AI client: {optimized_history}")

    try:
        logging.info("Creating message with AI client")
        response = await ai_client.create_message(
            prompt=user_input,
            system_message=system_message,
            tools=tools,
            history=optimized_history
        )
        logging.info("Message created successfully")

        if hasattr(response, 'usage'):
            if model_name.lower() in ['sonnet', 'claude-3-sonnet-20240229']:
                update_token_usage(model_name, {
                    "prompt_tokens": response.usage.input_tokens,
                    "completion_tokens": response.usage.output_tokens,
                    "total_tokens": response.usage.input_tokens + response.usage.output_tokens,
                    "cache_creation_tokens": getattr(response.usage, 'cache_creation_input_tokens', 0),
                    "cache_read_tokens": getattr(response.usage, 'cache_read_input_tokens', 0)
                })
            else:
                # For Llama and GPT
                update_token_usage(model_name, {
                    "prompt_tokens": response.usage.prompt_tokens,
                    "completion_tokens": response.usage.completion_tokens,
                    "total_tokens": response.usage.total_tokens
                })
    except Exception as e:
        logging.error(f"API Error: {str(e)}")
        return f"I'm sorry, there was an error communicating with the AI. Please try again. Error: {str(e)}"

    # Extract assistant response and function calls
    if hasattr(ai_client, 'extract_response'):
        assistant_response = ai_client.extract_response(response)
        if asyncio.iscoroutine(assistant_response):
            assistant_response = await assistant_response
    else:
        assistant_response = response.content[0].text if response.content else ""

    logging.info(f"EXTRACTED RESPONSE: {assistant_response}")

    # **Process file updates and modify assistant_response accordingly**
    assistant_response = await process_file_updates(assistant_response, file_context, app_state)

    # Extract and execute function calls
    function_calls = ai_client.extract_function_calls(response)
    function_results = await execute_function_calls(function_calls, tool_manager, model_name)

    # Integrate function results into assistant response
    if not assistant_response and function_results:
        assistant_response = "I've performed the following actions based on your request:\n\n"
    assistant_response = integrate_function_results(assistant_response, function_results, model_config)

    # **Append the finalized assistant response once**
    app_state.conversation_history.append({"role": "assistant", "content": assistant_response})
    logging.info(f"Final assistant response: {assistant_response}")
    return assistant_response


async def process_file_updates(assistant_response: str, file_context, app_state: Any) -> str:
    updated_files = detect_file_updates(assistant_response)
    notifications = []

    for relative_path, new_content in updated_files.items():
        existing_content = file_context.get_file_content(relative_path)

        if new_content is not None:
            if existing_content is not None:
                # Existing file: calculate diff
                diff = calculate_diff(existing_content, new_content)
                if diff.strip():
                    # Include acknowledgment and diff in the assistant response
                    ack_message = f"I've updated `{relative_path}` to the context."
                    diff_message = f"\n### Diff for {relative_path}\n```diff\n{diff}\n```"
                    assistant_response += f"\n{ack_message}{diff_message}"
                else:
                    # No changes detected
                    no_change_msg = f"\nNo changes detected in {relative_path}."
                    assistant_response += no_change_msg
            else:
                # First instance: notification about addition
                notification_message = f"\nI've added `{relative_path}` to the context."
                assistant_response += notification_message

            # Update the FileContextTracker with new content
            file_context.add_or_update_file(relative_path, new_content, "updated via function call")

            # Optionally, remove the processed content from assistant_response
            assistant_response = remove_full_content_from_response(assistant_response, relative_path)
        else:
            # Handle additions where no new content is provided
            notification_message = f"\nI've added `{relative_path}` to the context."
            assistant_response += notification_message

            # Update the FileContextTracker to reflect the addition
            file_context.add_or_update_file(relative_path, "", "added via function call")

    return assistant_response.strip()


def detect_file_updates(assistant_message: str) -> Dict[str, Optional[str]]:
    """
    Detects and extracts file updates from the assistant message.
    Returns a dictionary mapping relative_path to new_content or None for additions.
    """
    updates = {}

    # 1. Handle additions: "Added `file1.txt` to context."
    pattern_added = r"Added\s+`?(?P<path>[^`]+)`?\s+to context\."
    for match in re.finditer(pattern_added, assistant_message):
        relative_path = match.group('path').strip()
        updates[relative_path] = None  # None indicates addition without content
        logging.debug(f"Detected addition: {relative_path}")

    # 2. Handle fresh content: "Fresh content for file1.txt: ```diff\n...```"
    pattern_fresh = r"Fresh content for\s+`?(?P<path>[^`]+)`?:\s*```(?:diff|markdown)?\n(?P<content>[\s\S]*?)```"
    for match in re.finditer(pattern_fresh, assistant_message):
        relative_path = match.group('path').strip()
        content = match.group('content').strip()
        updates[relative_path] = content
        logging.debug(f"Detected fresh content for {relative_path}: {content}")

    # 3. Handle updates with diffs: "I've updated `file1.txt` to the context.\n### Diff for file1.txt\n```diff\n...```"
    pattern_update = r"I've updated\s+`?(?P<path>[^`]+)`?\s+to the context\.\n### Diff for\s+`?\1`?\s+```diff\n(?P<diff>[\s\S]*?)```"
    for match in re.finditer(pattern_update, assistant_message):
        relative_path = match.group('path').strip()
        diff = match.group('diff').strip()
        new_content = extract_new_content_from_diff(diff)
        if new_content:
            updates[relative_path] = new_content
            logging.debug(f"Detected update for {relative_path}: {new_content}")

    return updates


def extract_new_content_from_diff(diff: str) -> Optional[str]:
    """
    Parses a unified diff and extracts the new content.
    This function assumes that the diff follows the standard unified diff format.
    """
    new_lines = []
    for line in diff.splitlines():
        if line.startswith('+') and not line.startswith('+++'):
            new_lines.append(line[1:])
    return '\n'.join(new_lines) if new_lines else None

def calculate_diff(old_content: str, new_content: str) -> str:
    old_lines = old_content.splitlines()
    new_lines = new_content.splitlines()
    diff = difflib.unified_diff(old_lines, new_lines, lineterm='')
    return '\n'.join(diff)


def remove_full_content_from_response(assistant_message: str, relative_path: str) -> str:
    """
    Removes the full file content from the assistant message, leaving only the relevant response.
    """
    # Pattern captures only the 'Fresh content...' part
    pattern = rf"\s*Fresh content for {re.escape(relative_path)}:.*?```.*?\n[\s\S]*?```"
    cleaned_message = re.sub(pattern, "", assistant_message, flags=re.MULTILINE)
    # Replace multiple spaces with a single space and strip leading/trailing spaces
    cleaned_message = re.sub(r'\s{2,}', ' ', cleaned_message).strip()
    return cleaned_message


async def execute_function_calls(function_calls, tool_manager: ToolManager, model_name: str):
    results = []
    if asyncio.iscoroutine(function_calls):
        function_calls = await function_calls
    for function_call in function_calls:
        try:
            if isinstance(function_call, dict):
                # Llama format
                func_name = function_call.get('function')
                arguments = function_call.get('arguments', {})
            else:
                # GPT-4 format
                func_name = function_call.function.name
                arguments = json.loads(function_call.function.arguments)

            result = await tool_manager.execute_tool({"function": func_name, "arguments": arguments}, model_name)
            results.append((func_name, arguments, result))
        except Exception as e:
            logging.error(f"Error executing function: {str(e)}")
            results.append((func_name, arguments, f"Error executing function: {str(e)}"))
    return results


def integrate_function_results(assistant_response: str, function_results: List[Tuple[str, Dict, str]], model_config: Dict) -> str:
    if not assistant_response and function_results:
        assistant_response = "I've performed the following actions based on your request:\n\n"

    for func_name, args, result in function_results:
        if model_config.get('use_tool_calls', False):
            assistant_response += f"\n\nResult of {func_name}: {result}"
        else:
            # For models that embed function calls in the response
            placeholder = f"<function={func_name}>{json.dumps(args)}</function>"
            if placeholder in assistant_response:
                assistant_response = assistant_response.replace(placeholder, str(result))
            else:
                assistant_response += f"\n\nResult of {func_name}: {result}"

    return assistant_response
```


passing tests:

```
import pytest
from chat.chat import chat_with_model, process_file_updates, detect_file_updates, calculate_diff, remove_full_content_from_response
from app_state import get_app_state
from efc.efc_cache import get_file_context
from unittest.mock import AsyncMock, MagicMock


import pytest
from unittest.mock import MagicMock, AsyncMock

@pytest.fixture
def mock_app_state():
    class MockAppState:
        def __init__(self):
            self.active_project = "/path/to/active_project"
            self.conversation_history = []
            self.file_histories = {}
            self.token_usage = {}

        def get_active_project_tree_string(self):
            return "Mocked project tree"

    return MockAppState()


@pytest.fixture
def mock_file_context():
    class MockFileContextTracker:
        def __init__(self):
            self.tracked_files = {}

        def generate_context_awareness_prompt(self, app_state) -> str:
            if not self.tracked_files:
                return "No files in context."
            context = ""
            for relative_path in self.tracked_files:
                content = self.get_file_content(relative_path)
                context += f"- **{relative_path}**:\n```\n{content}\n```\n"
            return context

        def get_file_content(self, relative_path: str) -> str:
            return self.tracked_files.get(relative_path, "")

        def add_or_update_file(self, relative_path: str, content: str, operation: str) -> None:
            print(f"MockFileContextTracker: Adding '{relative_path}' with content '{content}' via operation '{operation}'")
            self.tracked_files[relative_path] = content


    return MockFileContextTracker()


def test_detect_file_updates():
    assistant_message = "Fresh content for file1.txt: ```diff\n+ Added line\n- Removed line\n```"
    updates = detect_file_updates(assistant_message)
    assert updates == {"file1.txt": "+ Added line\n- Removed line"}


def test_calculate_diff():
    old_content = "Line1\nLine2\nLine3"
    new_content = "Line1\nLine2 modified\nLine3"
    diff = calculate_diff(old_content, new_content)
    expected_diff = """--- 
+++ 
@@ -1,3 +1,3 @@
 Line1
-Line2
+Line2 modified
 Line3"""
    assert diff == expected_diff


def test_remove_full_content_from_response():
    assistant_message = "Some message. Fresh content for file1.txt: ```diff\n+ Added line\n- Removed line\n``` More text."
    cleaned_message = remove_full_content_from_response(assistant_message, 'file1.txt')
    assert cleaned_message == "Some message. More text."


@pytest.mark.asyncio
async def test_initial_file_addition(mock_app_state, mock_file_context, monkeypatch):
    user_input = "Create file1.txt"

    # 1. Mock AI client
    ai_client_mock = AsyncMock()

    # 2. Mock the response returned by create_message
    create_message_response_mock = MagicMock(
        usage=MagicMock(
            prompt_tokens=10,
            completion_tokens=20,
            total_tokens=30,
            cache_creation_input_tokens=5,
            cache_read_input_tokens=5
        )
    )
    ai_client_mock.create_message.return_value = create_message_response_mock

    # 3. Mock the extract_response method on the AI client with a relative path
    ai_client_mock.extract_response = AsyncMock(return_value="Added file1.txt to context.")

    # 4. Apply monkeypatch to replace AIClientFactory.get_ai_client with our mock
    monkeypatch.setattr("chat.chat.AIClientFactory.get_ai_client", lambda x: ai_client_mock)

    # 5. Apply monkeypatch to replace get_app_state to return mock_app_state
    monkeypatch.setattr("chat.chat.get_app_state", lambda: mock_app_state)

    # 6. Apply monkeypatch to replace get_file_context to return mock_file_context
    monkeypatch.setattr("chat.chat.get_file_context", lambda: mock_file_context)

    # 7. Call the function under test
    assistant_response = await chat_with_model(user_input, MagicMock(), "gpt-4")

    # 8. Print internal states for debugging
    print("Conversation History:", mock_app_state.conversation_history)
    print("Tracked Files:", mock_file_context.tracked_files)

    # 9. Check that the conversation_history has two messages
    expected_conversation_length = 2  # User input + Assistant response
    assert len(mock_app_state.conversation_history) == expected_conversation_length, \
        f"Expected {expected_conversation_length} messages in conversation_history, found {len(mock_app_state.conversation_history)}"

    # 10. Check the order and content of messages
    assert mock_app_state.conversation_history[0]['role'] == 'user', \
        f"Expected first message role to be 'user', got {mock_app_state.conversation_history[0]['role']}"

    assert mock_app_state.conversation_history[0]['content'] == 'Create file1.txt', \
        f"Unexpected user message content: {mock_app_state.conversation_history[0]['content']}"

    expected_assistant_content = "Added file1.txt to context.\nI've added `file1.txt` to the context."
    assert mock_app_state.conversation_history[1]['role'] == 'assistant', \
        f"Expected second message role to be 'assistant', got {mock_app_state.conversation_history[1]['role']}"

    assert mock_app_state.conversation_history[1]['content'] == expected_assistant_content, \
        f"Unexpected assistant message content: {mock_app_state.conversation_history[1]['content']}"

    # 11. Check that FileContextTracker was updated correctly
    assert "file1.txt" in mock_file_context.tracked_files, \
        f"Expected 'file1.txt' to be in tracked_files, got {list(mock_file_context.tracked_files.keys())}"

    assert mock_file_context.tracked_files["file1.txt"] == "", \
        f"Expected tracked_files['file1.txt'] to be an empty string, got {mock_file_context.tracked_files.get('file1.txt')}"


@pytest.mark.asyncio
async def test_file_update_with_diff(mock_app_state, mock_file_context, monkeypatch):
    # 1. Initial Setup: Add initial messages to conversation_history
    mock_app_state.conversation_history = [
        {'role': 'user', 'content': 'Create file1.txt'},
        {'role': 'assistant', 'content': "Added file1.txt to context.\nI've added `file1.txt` to the context."}
    ]
    
    # 2. Initialize tracked_files with existing content
    mock_file_context.tracked_files["file1.txt"] = "Initial content of file1"

    user_input = "Update file1.txt"

    # 3. Mock AI client
    ai_client_mock = AsyncMock()

    # 4. Mock the response returned by create_message with proper 'usage'
    create_message_response_mock = MagicMock(
        usage=MagicMock(
            prompt_tokens=15,
            completion_tokens=25,
            total_tokens=40,
            cache_creation_input_tokens=0,
            cache_read_input_tokens=0
        )
    )

    # 5. Set the mocked create_message to return our mocked response
    ai_client_mock.create_message.return_value = create_message_response_mock

    # 6. Mock the extract_response method on the response to return the expected string
    #    Use relative paths in the assistant's response and provide updated content without diff markers
    ai_client_mock.extract_response = AsyncMock(return_value=
        "Fresh content for file1.txt: ```diff\nThis is the updated content of file1\n```"
    )

    # 7. Apply monkeypatch to replace AIClientFactory.get_ai_client with our mock
    monkeypatch.setattr("chat.chat.AIClientFactory.get_ai_client", lambda x: ai_client_mock)

    # 8. Apply monkeypatch to replace get_app_state to return mock_app_state
    monkeypatch.setattr("chat.chat.get_app_state", lambda: mock_app_state)

    # 9. Apply monkeypatch to replace get_file_context to return mock_file_context
    monkeypatch.setattr("chat.chat.get_file_context", lambda: mock_file_context)

    # 10. Call the function under test
    assistant_response = await chat_with_model(user_input, MagicMock(), "gpt-4")
    
    # 11. Print internal states for debugging (optional)
    print("Conversation History:", mock_app_state.conversation_history)
    print("Tracked Files:", mock_file_context.tracked_files)
    
    # 12. Check that the conversation_history has four messages
    expected_conversation_length = 4  # Initial 2 + User input + Assistant response
    assert len(mock_app_state.conversation_history) == expected_conversation_length, \
        f"Expected {expected_conversation_length} messages in conversation_history, found {len(mock_app_state.conversation_history)}"
    
    # 13. Check that the fourth message is from the assistant
    assert mock_app_state.conversation_history[3]['role'] == 'assistant', \
        f"Expected fourth message role to be 'assistant', got {mock_app_state.conversation_history[3]['role']}"
    
    # 14. Check the assistant response content
    expected_assistant_content = (
        "I've updated `file1.txt` to the context."
        "\n### Diff for file1.txt\n```diff\n--- +++ @@ -1 +1 @@\n-Initial content of file1\n+This is the updated content of file1\n```"
    )
    assert mock_app_state.conversation_history[3]['content'] == expected_assistant_content, \
        f"Unexpected assistant message content: {mock_app_state.conversation_history[3]['content']}"
    
    # 15. Check that FileContextTracker was updated correctly
    assert "file1.txt" in mock_file_context.tracked_files, \
        f"Expected 'file1.txt' to be in tracked_files, got {list(mock_file_context.tracked_files.keys())}"
    
    assert mock_file_context.tracked_files["file1.txt"] == "This is the updated content of file1", \
        f"Expected tracked_files['file1.txt'] to be 'This is the updated content of file1', got '{mock_file_context.tracked_files.get('file1.txt')}'"

@pytest.mark.asyncio
async def test_multiple_file_operations(mock_app_state, mock_file_context, monkeypatch):
    # 1. Initial Setup: Add initial messages to conversation_history with relative paths
    mock_app_state.conversation_history = [
        {'role': 'user', 'content': 'Create file1.txt'},
        {'role': 'assistant', 'content': 'Added file1.txt to context.\nI\'ve added `file1.txt` to the context.'},
        {'role': 'user', 'content': 'Create file2.txt'},
        {'role': 'assistant', 'content': 'Added file2.txt to context.\nI\'ve added `file2.txt` to the context.'}
    ]
    
    # 2. Initialize tracked_files with existing content
    mock_file_context.tracked_files["file1.txt"] = "This is the content of file1"
    mock_file_context.tracked_files["file2.txt"] = "This is the content of file2"
    
    # 3. Define user inputs
    user_input1 = "Update file1.txt"
    user_input2 = "Update file2.txt"
    
    # 4. Mock AI client responses for both updates
    new_content1 = "This is the updated content of file1"
    ai_client_mock1 = AsyncMock()
    ai_client_mock1.create_message.return_value = MagicMock(
        usage=MagicMock(
            prompt_tokens=20,
            completion_tokens=30,
            total_tokens=50,
            cache_creation_input_tokens=0,
            cache_read_input_tokens=0
        )
    )
    ai_client_mock1.extract_response = AsyncMock(return_value=
        "Fresh content for file1.txt: ```diff\nThis is the updated content of file1\n```"
    )
    
    new_content2 = "This is the updated content of file2"
    ai_client_mock2 = AsyncMock()
    ai_client_mock2.create_message.return_value = MagicMock(
        usage=MagicMock(
            prompt_tokens=22,
            completion_tokens=28,
            total_tokens=50,
            cache_creation_input_tokens=0,
            cache_read_input_tokens=0
        )
    )
    ai_client_mock2.extract_response = AsyncMock(return_value=
        "Fresh content for file2.txt: ```diff\nThis is the updated content of file2\n```"
    )
    
    # 5. Setup AIClientFactory.get_ai_client to return the correct mock for each call
    ai_client_factory_mock = MagicMock()
    ai_client_factory_mock.get_ai_client.side_effect = [ai_client_mock1, ai_client_mock2]
    monkeypatch.setattr("chat.chat.AIClientFactory.get_ai_client", lambda x: ai_client_factory_mock.get_ai_client())
    
    # 6. Apply monkeypatch to replace get_app_state to return mock_app_state
    monkeypatch.setattr("chat.chat.get_app_state", lambda: mock_app_state)
    
    # 7. Apply monkeypatch to replace get_file_context to return mock_file_context
    monkeypatch.setattr("chat.chat.get_file_context", lambda: mock_file_context)
    
    # 8. Call the functions under test
    assistant_response1 = await chat_with_model(user_input1, MagicMock(), "gpt-4")
    assistant_response2 = await chat_with_model(user_input2, MagicMock(), "gpt-4")
    
    # 9. Print internal states for debugging (optional)
    print("Conversation History:", mock_app_state.conversation_history)
    print("Tracked Files:", mock_file_context.tracked_files)
    
    # 10. Check that conversation_history has 8 messages (initial 4 + 2 updates × 2 messages each)
    expected_conversation_length = 8
    assert len(mock_app_state.conversation_history) == expected_conversation_length, \
        f"Expected {expected_conversation_length} messages in conversation_history, found {len(mock_app_state.conversation_history)}"
    
    # 11. Check that the 6th and 8th messages (index 5 and 7) are assistant responses
    assistant_message_indices = [5, 7]
    for i in assistant_message_indices:
        assert mock_app_state.conversation_history[i]['role'] == 'assistant', \
            f"Expected message at index {i} to be from 'assistant', got '{mock_app_state.conversation_history[i]['role']}'"
    
    # 12. Check the assistant response contents
    expected_assistant_content1 = (
        "I've updated `file1.txt` to the context."
        "\n### Diff for file1.txt\n```diff\n--- +++ @@ -1 +1 @@\n-This is the content of file1\n+This is the updated content of file1\n```"
    )
    expected_assistant_content2 = (
        "I've updated `file2.txt` to the context."
        "\n### Diff for file2.txt\n```diff\n--- +++ @@ -1 +1 @@\n-This is the content of file2\n+This is the updated content of file2\n```"
    )
    
    assert mock_app_state.conversation_history[5]['content'] == expected_assistant_content1, \
        f"Unexpected assistant message content for file1.txt: {mock_app_state.conversation_history[5]['content']}"
    assert mock_app_state.conversation_history[7]['content'] == expected_assistant_content2, \
        f"Unexpected assistant message content for file2.txt: {mock_app_state.conversation_history[7]['content']}"
    
    # 13. Check that FileContextTracker was updated correctly
    assert "file1.txt" in mock_file_context.tracked_files, \
        f"Expected 'file1.txt' to be in tracked_files, got {list(mock_file_context.tracked_files.keys())}"
    assert "file2.txt" in mock_file_context.tracked_files, \
        f"Expected 'file2.txt' to be in tracked_files, got {list(mock_file_context.tracked_files.keys())}"
    
    assert mock_file_context.tracked_files["file1.txt"] == new_content1, \
        f"Expected tracked_files['file1.txt'] to be '{new_content1}', got '{mock_file_context.tracked_files.get('file1.txt')}'"
    assert mock_file_context.tracked_files["file2.txt"] == new_content2, \
        f"Expected tracked_files['file2.txt'] to be '{new_content2}', got '{mock_file_context.tracked_files.get('file2.txt')}'"


@pytest.mark.asyncio
async def test_conversation_history_alternates_roles(mock_app_state, mock_file_context, monkeypatch):
    """
    Test that the conversation history alternates between 'user' and 'assistant' roles correctly
    and that file operations are handled properly by checking for key substrings.
    """

    # 1. Initial Setup: Reset conversation_history and tracked_files
    mock_app_state.conversation_history = []
    mock_file_context.tracked_files = {}

    # 2. Define user inputs and expected assistant response substrings
    user_inputs = [
        'Hello',
        'Show me file1.txt',
        'Update file1.txt',
        "What's the weather like?"
    ]

    # Each entry contains substrings that should be present in the assistant's response
    expected_assistant_response_substrings = [
        ['Hi there!'],
        [
            'Added `file1.txt` to context.',
            "I've added `file1.txt` to the context."
        ],
        [
            "I've updated `file1.txt` to the context.",
            '### Diff for file1.txt',
            '```diff',
            '@@ -1 +1 @@',
            '-This is the content of file1',
            '+Updated content of file1',
            '```'
        ],
        ["I'm sorry, I don't have real-time weather information."]
    ]

    # 3. Mock AI clients for each interaction
    ai_client_mocks = []
    for i in range(4):
        ai_client_mock = AsyncMock()
        ai_client_mock.create_message.return_value = MagicMock(
            usage=MagicMock(
                input_tokens=10 + i*5,
                output_tokens=15 + i*5,
                total_tokens=25 + i*10,
                cache_creation_input_tokens=0,
                cache_read_input_tokens=0
            )
        )

        if i == 0:
            # 'Hello' interaction
            ai_client_mock.extract_response = AsyncMock(return_value='Hi there!')
            ai_client_mock.extract_function_calls = AsyncMock(return_value=[])
        elif i == 1:
            # 'Show me file1.txt' interaction
            ai_client_mock.extract_response = AsyncMock(
                return_value='Added `file1.txt` to context.\nFresh content for file1.txt: ```diff\nThis is the content of file1\n```'
            )
            ai_client_mock.extract_function_calls = AsyncMock(return_value=[])
        elif i == 2:
            # 'Update file1.txt' interaction
            ai_client_mock.extract_response = AsyncMock(
                return_value=(
                    "I've updated `file1.txt` to the context.\n"
                    "### Diff for file1.txt\n"
                    "```diff\n"
                    "@@ -1 +1 @@\n"
                    "-This is the content of file1\n"
                    "+Updated content of file1\n"
                    "```"
                )
            )
            ai_client_mock.extract_function_calls = AsyncMock(return_value=[])
        elif i == 3:
            # "What's the weather like?" interaction
            ai_client_mock.extract_response = AsyncMock(return_value="I'm sorry, I don't have real-time weather information.")
            ai_client_mock.extract_function_calls = AsyncMock(return_value=[])

        ai_client_mocks.append(ai_client_mock)

    # 4. Setup AIClientFactory.get_ai_client to return the correct mock for each interaction
    ai_client_factory_mock = MagicMock()
    ai_client_factory_mock.get_ai_client.side_effect = ai_client_mocks
    monkeypatch.setattr("chat.chat.AIClientFactory.get_ai_client", lambda x: ai_client_factory_mock.get_ai_client())

    # 5. Monkeypatch get_app_state and get_file_context to return the mocks
    monkeypatch.setattr("chat.chat.get_app_state", lambda: mock_app_state)
    monkeypatch.setattr("chat.chat.get_file_context", lambda: mock_file_context)

    # 6. Mock the add_or_update_file method to correctly set the content
    def mock_add_or_update_file(path, content, operation):
        mock_file_context.tracked_files[path] = content

    mock_file_context.add_or_update_file = MagicMock(side_effect=mock_add_or_update_file)

    # 6.a. Mock the get_file_content method to return the current tracked content
    mock_file_context.get_file_content = MagicMock(side_effect=lambda path: mock_file_context.tracked_files.get(path))

    # 7. Simulate User Interactions and Capture Assistant Responses
    for user_input in user_inputs:
        await chat_with_model(user_input, MagicMock(), "gpt-4")

    # 8. Print internal states for debugging (optional)
    print("Conversation History:", mock_app_state.conversation_history)
    print("Tracked Files:", mock_file_context.tracked_files)

    # 9. Assertions

    # 9.1. Check that conversation_history has 8 messages (4 interactions × 2 messages each)
    expected_conversation_length = 8
    assert len(mock_app_state.conversation_history) == expected_conversation_length, \
        f"Expected {expected_conversation_length} messages in conversation_history, found {len(mock_app_state.conversation_history)}"

    # 9.2. Verify that roles alternate correctly between 'user' and 'assistant'
    roles = [message['role'] for message in mock_app_state.conversation_history]
    for i in range(1, len(roles)):
        assert roles[i] != roles[i-1], \
            f"Conversation roles do not alternate at index {i}: {roles[i-1]} followed by {roles[i]}"

    # 9.3. Verify Assistant Responses with Substring Checks
    for idx, expected_substrings in zip([1, 3, 5, 7], expected_assistant_response_substrings):
        actual_content = mock_app_state.conversation_history[idx]['content']
        for substring in expected_substrings:
            assert substring in actual_content, \
                f"Expected substring '{substring}' not found in assistant message at index {idx}: {actual_content}"

        # 9.3.a. Ensure that no full content is present in 'Show me file1.txt' interaction (index=3)
        if idx == 3:
            assert "Fresh content for file1.txt" not in actual_content, \
                f"Unexpected full file content found in assistant message at index {idx}: {actual_content}"

    # 9.4. Verify FileContextTracker Updates
    assert "file1.txt" in mock_file_context.tracked_files, \
        f"Expected 'file1.txt' to be in tracked_files, got {list(mock_file_context.tracked_files.keys())}"
    assert mock_file_context.tracked_files["file1.txt"] == "Updated content of file1", \
        f"Expected tracked_files['file1.txt'] to be 'Updated content of file1', got '{mock_file_context.tracked_files.get('file1.txt')}'"

    # 9.5. Ensure No Unexpected Files are Tracked
    assert len(mock_file_context.tracked_files) == 1, \
        f"Expected only 'file1.txt' in tracked_files, got {list(mock_file_context.tracked_files.keys())}"
```