import copy
import json
import os
import tempfile
from datetime import datetime
from unittest.mock import MagicMock, patch

import pytest
from click.testing import CliRunner

from src.open_amazon_chat_completions_server.cli.main import cli


@pytest.fixture
def runner():
    return CliRunner()


@pytest.fixture
def mock_env_file():
    with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
        f.write("API_KEY=test-key\n")
        f.write("OPENAI_API_KEY=test-openai-key\n")
        f.write("AWS_REGION=us-east-1\n")
        f.write("SERVER_URL=http://localhost:8000\n")
    yield f.name
    os.unlink(f.name)


@pytest.fixture
def mock_websocket():
    class AsyncContextManagerMock:
        async def __aenter__(self):
            return self

        async def __aexit__(self, exc_type, exc_val, exc_tb):
            pass

        async def send(self, message):
            pass

        async def __aiter__(self):
            yield json.dumps(
                {"choices": [{"delta": {"content": "Hello! How can I help you?"}}]}
            )

    return AsyncContextManagerMock()


@pytest.fixture
def mock_history_dir(tmp_path):
    history_dir = tmp_path / "chat_history"
    history_dir.mkdir(parents=True, exist_ok=True)
    old_dir = os.environ.get("CHAT_HISTORY_DIR")
    os.environ["CHAT_HISTORY_DIR"] = str(history_dir)
    yield history_dir
    if old_dir:
        os.environ["CHAT_HISTORY_DIR"] = old_dir
    else:
        del os.environ["CHAT_HISTORY_DIR"]


def test_config_show(runner, mock_env_file):
    with patch(
        "src.open_amazon_chat_completions_server.cli.main.DOTENV_PATH", mock_env_file
    ):
        result = runner.invoke(cli, ["config", "show"])
        assert result.exit_code == 0
        assert "API_KEY" in result.output
        assert "OPENAI_API_KEY" in result.output
        assert "********" in result.output  # Sensitive values should be masked


def test_config_set(runner):
    with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
        env_file = f.name

    try:
        with patch(
            "src.open_amazon_chat_completions_server.cli.main.DOTENV_PATH", env_file
        ):
            # Test setting individual values
            result = runner.invoke(cli, ["config", "set", "API_KEY", "test-key"])
            assert result.exit_code == 0

            # Verify the file was created with correct value
            with open(env_file) as f:
                content = f.read()
                assert "API_KEY=test-key" in content
    finally:
        os.unlink(env_file)


@patch("src.open_amazon_chat_completions_server.cli.main.make_api_request")
@patch("src.open_amazon_chat_completions_server.cli.main.ChatHistoryManager")
def test_chat_command_non_streaming(
    mock_manager_class, mock_make_request, runner, mock_env_file
):
    with patch(
        "src.open_amazon_chat_completions_server.cli.main.DOTENV_PATH", mock_env_file
    ):
        # Mock the ChatHistoryManager
        mock_manager = MagicMock()
        mock_manager_class.return_value = mock_manager
        mock_session = MagicMock()
        mock_messages = MagicMock()
        mock_messages.__iter__.return_value = []
        mock_session.messages = mock_messages
        mock_session.id = "test-id"
        mock_session.name = None
        mock_manager.create_new_session.return_value = mock_session

        # Setup mock response
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.ok = True  # Ensure the mock response is considered 'ok'
        mock_response.json.return_value = {
            "choices": [{"message": {"content": "Hello! How can I help you?"}}]
        }
        mock_response.raise_for_status.return_value = (
            None  # Prevent raise_for_status from erroring
        )
        mock_make_request.return_value = mock_response
        mock_make_request.side_effect = None  # Ensure no side effects

        # Test single message exchange with explicit API key
        result = runner.invoke(
            cli,
            ["chat", "--model", "test-model", "--api-key", "test-key", "--no-stream"],
            input="Hello\nexit\n",
        )

        # Verify the result
        assert result.exit_code == 0
        assert "Hello! How can I help you?" in result.output

        # Verify the mock was called correctly
        mock_make_request.assert_called_once_with(
            "http://localhost:8000/v1/chat/completions",
            method="POST",
            json={
                "model": "test-model",
                "messages": [
                    {"role": "user", "content": "Hello"},
                    {"role": "assistant", "content": "Hello! How can I help you?"},
                ],
                "stream": False,
            },
            headers={
                "Authorization": "Bearer test-key",
                "Content-Type": "application/json",
            },
        )


@pytest.mark.skip(reason="Complex to mock dynamic httpx import - needs refactoring")
def test_chat_command_streaming(runner):
    """Test streaming chat command - currently skipped due to mocking complexity"""
    pass


@patch("src.open_amazon_chat_completions_server.cli.main.make_api_request")
@patch("src.open_amazon_chat_completions_server.cli.main.ChatHistoryManager")
def test_chat_command_tool_calls(
    mock_manager_class, mock_make_request, runner, mock_env_file
):
    with patch(
        "src.open_amazon_chat_completions_server.cli.main.DOTENV_PATH", mock_env_file
    ):
        mock_manager = MagicMock()
        mock_manager_class.return_value = mock_manager
        mock_session = MagicMock()
        mock_session.messages = []
        mock_session.id = "test-id"
        mock_session.name = None
        mock_manager.create_new_session.return_value = mock_session

        mock_response1_data = {
            "choices": [
                {
                    "message": {
                        "tool_calls": [
                            {
                                "id": "call_123",
                                "function": {
                                    "name": "search",
                                    "arguments": '{"query": "test"}',
                                },
                            }
                        ]
                    }
                }
            ]
        }
        mock_response1 = MagicMock()
        mock_response1.status_code = 200
        mock_response1.ok = True
        mock_response1.json.return_value = mock_response1_data
        mock_response1.raise_for_status.return_value = None

        mock_response2_data = {
            "choices": [{"message": {"content": "Based on the search results..."}}]
        }
        mock_response2 = MagicMock()
        mock_response2.status_code = 200
        mock_response2.ok = True
        mock_response2.json.return_value = mock_response2_data
        mock_response2.raise_for_status.return_value = None

        actual_messages_payloads_sent = []
        responses_to_return = [mock_response1, mock_response2]

        def custom_side_effect(*args, **kwargs):
            if "json" in kwargs and "messages" in kwargs["json"]:
                actual_messages_payloads_sent.append(
                    copy.deepcopy(kwargs["json"]["messages"])
                )
            else:
                actual_messages_payloads_sent.append(None)

            if not responses_to_return:
                raise AssertionError(
                    "Mock make_api_request called more times than expected or responses_to_return list exhausted."
                )
            return responses_to_return.pop(0)

        mock_make_request.side_effect = custom_side_effect

        result = runner.invoke(
            cli,
            ["chat", "--model", "test-model", "--api-key", "test-key", "--no-stream"],
            input="Hello\nSearch Result\nexit\n",
        )

        assert result.exit_code == 0, (
            f"CLI exited with error: {result.output} {result.exception} traceback: {result.exc_info}"
        )
        assert mock_make_request.call_count == 2
        assert "I need to use some tools" in result.output
        assert "Based on the search results..." in result.output

        all_call_args = mock_make_request.call_args_list
        assert len(all_call_args) == 2
        assert len(actual_messages_payloads_sent) == 2

        # First call assertions
        first_call_args = all_call_args[0]
        assert first_call_args.args[0] == "http://localhost:8000/v1/chat/completions"
        assert first_call_args.kwargs["method"] == "POST"
        assert first_call_args.kwargs["json"]["model"] == "test-model"
        assert actual_messages_payloads_sent[0] == [
            {"role": "user", "content": "Hello"}
        ]

        # Second call assertions
        second_call_args = all_call_args[1]
        assert second_call_args.args[0] == "http://localhost:8000/v1/chat/completions"
        assert second_call_args.kwargs["method"] == "POST"
        assert second_call_args.kwargs["json"]["model"] == "test-model"
        # This reflects the current CLI behavior where "Search Result" is appended as a new user message
        # in the next loop iteration after being consumed as a tool response.
        assert actual_messages_payloads_sent[1] == [
            {"role": "user", "content": "Hello"},
            {"role": "user", "content": "Search Result"},
        ]


@patch("requests.get")
def test_models_command(mock_get, runner):
    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "object": "list",
        "data": [
            {"id": "model1", "owned_by": "owner1", "created": 123456789},
            {"id": "model2", "owned_by": "owner2", "created": 123456790},
        ],
    }
    mock_get.return_value = mock_response

    result = runner.invoke(cli, ["models", "--api-key", "test-key"])

    assert result.exit_code == 0
    assert "model1" in result.output
    assert "model2" in result.output
    assert "owner1" in result.output
    assert "owner2" in result.output


@patch("uvicorn.run")
def test_serve_command(mock_run, runner):
    # Test the serve command
    result = runner.invoke(cli, ["serve", "--host", "localhost", "--port", "8000"])
    assert result.exit_code == 0

    # Verify uvicorn.run was called with correct arguments
    mock_run.assert_called_once_with(
        "src.open_amazon_chat_completions_server.api.app:app",
        host="localhost",
        port=8000,
        reload=False,
        env_file=None,
    )


@patch("src.open_amazon_chat_completions_server.cli.main.ChatHistoryManager")
def test_history_list_command(mock_manager_class, runner, mock_history_dir):
    """Test listing chat history when no sessions exist."""
    # Mock the ChatHistoryManager
    mock_manager = MagicMock()
    mock_manager.list_sessions.return_value = []
    mock_manager_class.return_value = mock_manager

    result = runner.invoke(cli, ["history", "list"])
    assert result.exit_code == 0
    # Check for the message indicating no sessions were found
    assert "No chat sessions found." in result.output
    # The following assertions for table headers are not applicable if no table is printed
    # assert "ID" in result.output
    # assert "Name" in result.output
    # assert "Model" in result.output
    # assert "Messages" in result.output
    # assert "Updated" in result.output
    # A more specific check for the empty table could be added if needed,
    # e.g., checking the exact number of lines or specific border characters.


@patch("src.open_amazon_chat_completions_server.cli.main.ChatHistoryManager")
def test_history_list_command_with_sessions(
    mock_manager_class, runner, mock_history_dir
):
    """Test listing chat history when sessions exist."""
    # Mock the ChatHistoryManager
    mock_manager = MagicMock()
    mock_session = MagicMock()
    mock_session.id = "test-id"
    mock_session.name = "Test Session"
    mock_session.model = "test-model"
    # Add a mock message to messages list
    mock_message = MagicMock()
    mock_message.role = "user"
    mock_message.content = "Hello"
    mock_session.messages = [mock_message]
    mock_session.updated_at = datetime.now()
    mock_manager.list_sessions.return_value = [mock_session]
    mock_manager_class.return_value = mock_manager

    result = runner.invoke(cli, ["history", "list"])
    assert result.exit_code == 0
    mock_manager_class.assert_called_once()  # Verify the mock class was instantiated
    mock_manager.list_sessions.assert_called_once()  # Verify the list_sessions method on the instance was called
    assert mock_session.id in result.output
    assert mock_session.name in result.output
    assert mock_session.model in result.output


def test_history_export_command(runner, tmp_path):
    # Create a temporary chat history
    history_dir = tmp_path / "chat_history"
    os.environ["CHAT_HISTORY_DIR"] = str(history_dir)

    result = runner.invoke(cli, ["history", "export", "nonexistent-id"])
    assert result.exit_code == 0
    assert "Error" in result.output


def test_history_delete_command(runner, tmp_path):
    # Create a temporary chat history
    history_dir = tmp_path / "chat_history"
    os.environ["CHAT_HISTORY_DIR"] = str(history_dir)

    result = runner.invoke(cli, ["history", "delete", "nonexistent-id"])
    assert result.exit_code == 0
    assert "Error" in result.output
