from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest
from django.test import Client
from django.urls import reverse

from django_anyllm.models import Chat, Message


@pytest.fixture
def client():
    return Client()


@pytest.fixture
def chat(db):
    return Chat.objects.create(title="Test Chat", model="gpt-4", provider="openai")


class TestChatListView:
    """Tests for the chat list view."""

    @pytest.mark.django_db
    def test_list(self, client, chat):
        response = client.get(reverse("django_anyllm:chat_list"))
        assert response.status_code == 200
        assert b"Test Chat" in response.content


class TestChatCreateView:
    """Tests for creating a new chat."""

    @pytest.mark.django_db
    def test_create(self, client):
        response = client.post(reverse("django_anyllm:chat_create"))
        assert response.status_code == 302
        assert Chat.objects.count() == 1
        chat = Chat.objects.first()
        assert chat.title == ""
        assert response.url == reverse("django_anyllm:chat_detail", args=[chat.pk])


class TestChatDeleteView:
    """Tests for deleting a chat."""

    @pytest.mark.django_db
    def test_delete(self, client, chat):
        response = client.post(reverse("django_anyllm:chat_delete", args=[chat.pk]))
        assert response.status_code == 200
        assert not Chat.objects.filter(pk=chat.pk).exists()

    @pytest.mark.django_db
    def test_delete_redirects_when_from_active_chat(self, client, chat):
        response = client.post(
            reverse("django_anyllm:chat_delete", args=[chat.pk]),
            {"from_chat": chat.pk},
        )
        assert response.status_code == 200
        assert response["HX-Redirect"] == reverse("django_anyllm:chat_list")

    @pytest.mark.django_db
    def test_delete_no_redirect_when_from_other_chat(self, client, chat):
        other = Chat.objects.create(title="Other")
        response = client.post(
            reverse("django_anyllm:chat_delete", args=[chat.pk]),
            {"from_chat": other.pk},
        )
        assert response.status_code == 200
        assert "HX-Redirect" not in response


class TestChatDetailView:
    """Tests for the chat detail view."""

    @pytest.mark.django_db
    def test_detail(self, client, chat):
        response = client.get(reverse("django_anyllm:chat_detail", args=[chat.pk]))
        assert response.status_code == 200
        assert b"Test Chat" in response.content


class TestMessageCreateView:
    """Tests for posting a message."""

    @pytest.mark.django_db
    def test_post_message(self, client, chat):
        response = client.post(
            reverse("django_anyllm:message_create", args=[chat.pk]),
            {"content": "Hello!"},
        )
        assert response.status_code == 200
        assert chat.messages.filter(role=Message.Role.USER, content="Hello!").exists()
        assert chat.messages.filter(role=Message.Role.ASSISTANT, content="").exists()

    @pytest.mark.django_db
    def test_post_message_auto_titles_chat(self, client):
        chat = Chat.objects.create()
        response = client.post(
            reverse("django_anyllm:message_create", args=[chat.pk]),
            {"content": "What is the meaning of life?"},
        )
        assert response.status_code == 200
        chat.refresh_from_db()
        assert chat.title == "What is the meaning of life?"

    @pytest.mark.django_db
    def test_post_message_does_not_overwrite_existing_title(self, client, chat):
        response = client.post(
            reverse("django_anyllm:message_create", args=[chat.pk]),
            {"content": "A new message"},
        )
        assert response.status_code == 200
        chat.refresh_from_db()
        assert chat.title == "Test Chat"

    @pytest.mark.django_db
    def test_post_message_truncates_long_title(self, client):
        chat = Chat.objects.create()
        long_content = "A" * 100
        response = client.post(
            reverse("django_anyllm:message_create", args=[chat.pk]),
            {"content": long_content},
        )
        assert response.status_code == 200
        chat.refresh_from_db()
        assert chat.title == "A" * 57 + "..."

    @pytest.mark.django_db
    def test_post_with_files(self, client, chat):
        from django.core.files.uploadedfile import SimpleUploadedFile

        image = SimpleUploadedFile(
            "test.png", b"fake-image-data", content_type="image/png"
        )
        response = client.post(
            reverse("django_anyllm:message_create", args=[chat.pk]),
            {"content": "Look at this", "file": image},
        )
        assert response.status_code == 200
        user_msg = chat.messages.get(role=Message.Role.USER)
        assert user_msg.files.count() == 1
        assert user_msg.files.first().mime_type == "image/png"

    @pytest.mark.django_db
    def test_post_with_multiple_files(self, client, chat):
        from django.core.files.uploadedfile import SimpleUploadedFile

        image = SimpleUploadedFile(
            "test.png", b"fake-image-data", content_type="image/png"
        )
        text = SimpleUploadedFile(
            "notes.txt", b"hello world", content_type="text/plain"
        )
        response = client.post(
            reverse("django_anyllm:message_create", args=[chat.pk]),
            {"content": "Look at these", "file": [image, text]},
        )
        assert response.status_code == 200
        user_msg = chat.messages.get(role=Message.Role.USER)
        assert user_msg.files.count() == 2
        mime_types = {f.mime_type for f in user_msg.files.all()}
        assert mime_types == {"image/png", "text/plain"}

    @pytest.mark.django_db
    def test_post_with_non_image_file_includes_file_content_part(self, client, chat):
        from django.core.files.uploadedfile import SimpleUploadedFile
        from django_anyllm.views import _db_message_to_llm

        text = SimpleUploadedFile(
            "notes.txt", b"hello world", content_type="text/plain"
        )
        response = client.post(
            reverse("django_anyllm:message_create", args=[chat.pk]),
            {"content": "Read this", "file": text},
        )
        assert response.status_code == 200
        user_msg = chat.messages.get(role=Message.Role.USER)
        llm_msg = _db_message_to_llm(user_msg)
        assert isinstance(llm_msg["content"], list)
        parts = llm_msg["content"]
        assert parts[0]["type"] == "text"
        assert parts[0]["text"].startswith("Read this")
        assert "file_id=" in parts[0]["text"]
        file_parts = [p for p in parts if p.get("type") == "file"]
        assert len(file_parts) == 1
        assert file_parts[0]["file"]["filename"].endswith(".txt")


class TestChatStreamView:
    """Tests for the SSE streaming endpoint."""

    def _make_chunk(self, content=None, reasoning=None):
        chunk = MagicMock()
        delta = MagicMock()
        delta.content = content
        delta.reasoning = reasoning
        delta.tool_calls = None
        choice = MagicMock()
        choice.delta = delta
        chunk.choices = [choice]
        return chunk

    @pytest.mark.django_db
    def test_stream_content(self, client, chat):
        assistant = chat.messages.create(role=Message.Role.ASSISTANT, content="")
        chat.messages.create(role=Message.Role.USER, content="Hi")

        chunks = [
            self._make_chunk("Hello"),
            self._make_chunk(" world"),
        ]

        with patch(
            "django_anyllm.views.completion",
            return_value=iter(chunks),
        ):
            response = client.get(
                reverse("django_anyllm:chat_stream", args=[chat.pk]),
                {"message": assistant.pk},
            )
            content = b"".join(response.streaming_content).decode()

        assert response.status_code == 200
        assert response["Content-Type"] == "text/event-stream"
        assert "event: content" in content
        assert "data: Hello" in content
        assert "data:  world" in content
        assert "event: done" in content

        assistant.refresh_from_db()
        assert assistant.content == "Hello world"

    @pytest.mark.django_db
    def test_stream_reasoning(self, client, chat):
        assistant = chat.messages.create(role=Message.Role.ASSISTANT, content="")
        chat.messages.create(role=Message.Role.USER, content="Hi")

        reasoning = MagicMock()
        reasoning.content = "Thinking..."
        chunks = [self._make_chunk("OK", reasoning=reasoning)]

        with patch(
            "django_anyllm.views.completion",
            return_value=iter(chunks),
        ):
            response = client.get(
                reverse("django_anyllm:chat_stream", args=[chat.pk]),
                {"message": assistant.pk},
            )
            content = b"".join(response.streaming_content).decode()

        assert "event: reasoning" in content
        assert "data: Thinking..." in content

    @pytest.mark.django_db
    def test_stream_with_tool_calls(self, client, chat):
        assistant = chat.messages.create(role=Message.Role.ASSISTANT, content="")
        chat.messages.create(role=Message.Role.USER, content="List authors")

        # First round: tool call request.
        round1 = [
            self._make_chunk(None),
            self._make_chunk(None),
        ]
        # Set up tool call deltas.
        tc1 = MagicMock()
        tc1.index = 0
        tc1.id = "tc_1"
        tc1.function = MagicMock()
        tc1.function.name = "_list_registered_models"
        tc1.function.arguments = ""
        round1[0].choices[0].delta.tool_calls = [tc1]
        tc2 = MagicMock()
        tc2.index = 0
        tc2.id = ""
        tc2.function = MagicMock()
        tc2.function.name = ""
        tc2.function.arguments = "{}"
        round1[1].choices[0].delta.tool_calls = [tc2]

        # Second round: final response.
        round2 = [self._make_chunk("Done")]

        with patch(
            "django_anyllm.views.completion",
            side_effect=[iter(round1), iter(round2)],
        ):
            response = client.get(
                reverse("django_anyllm:chat_stream", args=[chat.pk]),
                {"message": assistant.pk},
            )
            content = b"".join(response.streaming_content).decode()

        assert "event: tool_call" in content
        assert "event: tool_result" in content
        assert "event: content" in content
        assert "event: done" in content

    @pytest.mark.django_db
    def test_stream_skips_empty_assistant_messages_in_history(self, client, chat):
        # Simulate a previous failed stream leaving an empty assistant message.
        chat.messages.create(role=Message.Role.ASSISTANT, content="")
        chat.messages.create(role=Message.Role.USER, content="First")

        # Current streaming session.
        assistant = chat.messages.create(role=Message.Role.ASSISTANT, content="")
        chat.messages.create(role=Message.Role.USER, content="Second")

        chunks = [self._make_chunk("Response")]
        captured_messages: list[list[dict]] = []

        def _capture_and_return(*args, **kwargs):
            # Copy the list at call time because the view mutates it later.
            captured_messages.append(list(kwargs["messages"]))
            return iter(chunks)

        with patch(
            "django_anyllm.views.completion",
            side_effect=_capture_and_return,
        ):
            response = client.get(
                reverse("django_anyllm:chat_stream", args=[chat.pk]),
                {"message": assistant.pk},
            )
            b"".join(response.streaming_content)

        call_messages = captured_messages[0]
        assistant_messages = [m for m in call_messages if m.get("role") == "assistant"]
        assert len(assistant_messages) == 0

    @pytest.mark.django_db
    def test_stream_handles_iteration_exception(self, client, chat):
        assistant = chat.messages.create(role=Message.Role.ASSISTANT, content="")
        chat.messages.create(role=Message.Role.USER, content="Hi")

        def failing_iter():
            yield self._make_chunk("Hello")
            raise RuntimeError("Stream broke")

        with patch(
            "django_anyllm.views.completion",
            return_value=failing_iter(),
        ):
            response = client.get(
                reverse("django_anyllm:chat_stream", args=[chat.pk]),
                {"message": assistant.pk},
            )
            content = b"".join(response.streaming_content).decode()

        assert "event: error" in content
        assert "Stream broke" in content
        assert "event: done" in content
