"""
Tests for the Chat History system — CRUD endpoints and /chat integration.

Covers:
    1. Chat CRUD (POST/GET/PUT/DELETE /chats)
    2. /chat endpoint with chat_id persistence
    3. Auto-title generation
    4. Index consistency
    5. Storage helpers
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from runtime.config import RuntimeConfig


# ═══════════════════════════════════════════════════════════════════════
# Fixtures
# ═══════════════════════════════════════════════════════════════════════


@pytest.fixture()
def runtime_config(tmp_path: Path) -> RuntimeConfig:
    """Create a test config with tmp_path as data directory."""
    return RuntimeConfig(
        cloud_url="https://mock.brynq.ai",
        data_dir=tmp_path,
        ollama_url="http://localhost:11434",
        debug=True,
    )


@pytest.fixture()
def test_client(runtime_config: RuntimeConfig) -> Any:
    """Create a FastAPI TestClient for the runtime app."""
    from fastapi.testclient import TestClient

    from runtime.app import create_app

    app = create_app(runtime_config)
    return TestClient(app)


# ═══════════════════════════════════════════════════════════════════════
# 1. Storage Helpers
# ═══════════════════════════════════════════════════════════════════════


class TestStorageHelpers:
    """Tests for the chat storage helper functions."""

    def test_create_chat_returns_valid_object(self, runtime_config: RuntimeConfig) -> None:
        from runtime.app import _create_chat

        chat = _create_chat(runtime_config.data_dir)
        assert chat["id"].startswith("chat_")
        assert len(chat["id"]) == 17  # "chat_" + 12 hex chars
        assert chat["title"] is None
        assert chat["project_id"] is None
        assert chat["messages"] == []
        assert "created_at" in chat
        assert "updated_at" in chat

    def test_create_chat_with_title_and_project(self, runtime_config: RuntimeConfig) -> None:
        from runtime.app import _create_chat

        chat = _create_chat(
            runtime_config.data_dir,
            title="My Chat",
            project_id="proj_abc123",
        )
        assert chat["title"] == "My Chat"
        assert chat["project_id"] == "proj_abc123"

    def test_create_chat_persists_to_disk(self, runtime_config: RuntimeConfig) -> None:
        from runtime.app import _create_chat, _read_chat

        chat = _create_chat(runtime_config.data_dir, title="Persisted")
        loaded = _read_chat(runtime_config.data_dir, chat["id"])
        assert loaded is not None
        assert loaded["title"] == "Persisted"

    def test_create_chat_updates_index(self, runtime_config: RuntimeConfig) -> None:
        from runtime.app import _create_chat, _read_chat_index

        _create_chat(runtime_config.data_dir, title="First")
        _create_chat(runtime_config.data_dir, title="Second")

        index = _read_chat_index(runtime_config.data_dir)
        assert len(index) == 2
        titles = {e["title"] for e in index}
        assert "First" in titles
        assert "Second" in titles

    def test_read_nonexistent_chat_returns_none(self, runtime_config: RuntimeConfig) -> None:
        from runtime.app import _read_chat

        assert _read_chat(runtime_config.data_dir, "chat_doesnotexist") is None

    def test_delete_chat_file(self, runtime_config: RuntimeConfig) -> None:
        from runtime.app import _create_chat, _delete_chat_file, _read_chat

        chat = _create_chat(runtime_config.data_dir)
        _delete_chat_file(runtime_config.data_dir, chat["id"])
        assert _read_chat(runtime_config.data_dir, chat["id"]) is None

    def test_update_chat_index_entry(self, runtime_config: RuntimeConfig) -> None:
        from runtime.app import _create_chat, _read_chat_index, _update_chat_index_entry

        chat = _create_chat(runtime_config.data_dir, title="Old Title")
        _update_chat_index_entry(
            runtime_config.data_dir,
            chat["id"],
            title="New Title",
            last_message="hello world",
        )

        index = _read_chat_index(runtime_config.data_dir)
        entry = next(e for e in index if e["id"] == chat["id"])
        assert entry["title"] == "New Title"
        assert entry["last_message"] == "hello world"

    def test_auto_title_short_message(self) -> None:
        from runtime.app import _auto_title

        assert _auto_title("What is Python?") == "What is Python?"

    def test_auto_title_long_message_truncated(self) -> None:
        from runtime.app import _auto_title

        long_msg = "A" * 100
        title = _auto_title(long_msg)
        assert len(title) == 53  # 50 chars + "..."
        assert title.endswith("...")

    def test_auto_title_strips_newlines(self) -> None:
        from runtime.app import _auto_title

        title = _auto_title("Line one\nLine two\nLine three")
        assert "\n" not in title
        assert title == "Line one Line two Line three"

    def test_empty_index_on_fresh_dir(self, runtime_config: RuntimeConfig) -> None:
        from runtime.app import _read_chat_index

        index = _read_chat_index(runtime_config.data_dir)
        assert index == []


# ═══════════════════════════════════════════════════════════════════════
# 2. Chat CRUD Endpoints
# ═══════════════════════════════════════════════════════════════════════


class TestCreateChatEndpoint:
    """Tests for POST /chats."""

    def test_create_chat_default(self, test_client: Any) -> None:
        resp = test_client.post("/chats", json={})
        assert resp.status_code == 201
        data = resp.json()
        assert data["id"].startswith("chat_")
        assert data["title"] is None
        assert data["project_id"] is None
        assert data["messages"] == []
        assert "created_at" in data
        assert "updated_at" in data

    def test_create_chat_with_title(self, test_client: Any) -> None:
        resp = test_client.post("/chats", json={"title": "Q4 Revenue Analysis"})
        assert resp.status_code == 201
        assert resp.json()["title"] == "Q4 Revenue Analysis"

    def test_create_chat_with_project(self, test_client: Any) -> None:
        resp = test_client.post(
            "/chats",
            json={"title": "Research", "project_id": "proj_xyz"},
        )
        assert resp.status_code == 201
        data = resp.json()
        assert data["title"] == "Research"
        assert data["project_id"] == "proj_xyz"


class TestListChatsEndpoint:
    """Tests for GET /chats."""

    def test_list_empty(self, test_client: Any) -> None:
        resp = test_client.get("/chats")
        assert resp.status_code == 200
        data = resp.json()
        assert data["chats"] == []
        assert data["count"] == 0

    def test_list_returns_all_chats(self, test_client: Any) -> None:
        test_client.post("/chats", json={"title": "Chat A"})
        test_client.post("/chats", json={"title": "Chat B"})
        test_client.post("/chats", json={"title": "Chat C"})

        resp = test_client.get("/chats")
        data = resp.json()
        assert data["count"] == 3

    def test_list_sorted_by_updated_at_desc(self, test_client: Any) -> None:
        """Most recently updated chat should appear first."""
        test_client.post("/chats", json={"title": "Older"})
        test_client.post("/chats", json={"title": "Newer"})

        resp = test_client.get("/chats")
        chats = resp.json()["chats"]
        assert len(chats) == 2
        # "Newer" was created second, so it has a later updated_at
        assert chats[0]["title"] == "Newer"
        assert chats[1]["title"] == "Older"


class TestGetChatEndpoint:
    """Tests for GET /chats/{id}."""

    def test_get_existing_chat(self, test_client: Any) -> None:
        create_resp = test_client.post("/chats", json={"title": "Test Chat"})
        chat_id = create_resp.json()["id"]

        resp = test_client.get(f"/chats/{chat_id}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == chat_id
        assert data["title"] == "Test Chat"
        assert data["messages"] == []

    def test_get_nonexistent_chat(self, test_client: Any) -> None:
        resp = test_client.get("/chats/chat_doesnotexist")
        assert resp.status_code == 404


class TestDeleteChatEndpoint:
    """Tests for DELETE /chats/{id}."""

    def test_delete_existing_chat(self, test_client: Any) -> None:
        create_resp = test_client.post("/chats", json={"title": "To Delete"})
        chat_id = create_resp.json()["id"]

        resp = test_client.delete(f"/chats/{chat_id}")
        assert resp.status_code == 200
        assert resp.json()["deleted"] is True
        assert resp.json()["id"] == chat_id

        # Verify it's gone
        assert test_client.get(f"/chats/{chat_id}").status_code == 404

    def test_delete_removes_from_index(self, test_client: Any) -> None:
        test_client.post("/chats", json={"title": "Keep"})
        create_resp = test_client.post("/chats", json={"title": "Delete Me"})
        chat_id = create_resp.json()["id"]

        test_client.delete(f"/chats/{chat_id}")

        resp = test_client.get("/chats")
        assert resp.json()["count"] == 1
        assert resp.json()["chats"][0]["title"] == "Keep"

    def test_delete_nonexistent_chat(self, test_client: Any) -> None:
        resp = test_client.delete("/chats/chat_doesnotexist")
        assert resp.status_code == 404


class TestUpdateChatEndpoint:
    """Tests for PUT /chats/{id}."""

    def test_update_title(self, test_client: Any) -> None:
        create_resp = test_client.post("/chats", json={"title": "Original"})
        chat_id = create_resp.json()["id"]

        resp = test_client.put(f"/chats/{chat_id}", json={"title": "Updated Title"})
        assert resp.status_code == 200
        assert resp.json()["title"] == "Updated Title"

        # Verify persisted
        get_resp = test_client.get(f"/chats/{chat_id}")
        assert get_resp.json()["title"] == "Updated Title"

    def test_update_project_id(self, test_client: Any) -> None:
        create_resp = test_client.post("/chats", json={})
        chat_id = create_resp.json()["id"]

        resp = test_client.put(
            f"/chats/{chat_id}",
            json={"project_id": "proj_new"},
        )
        assert resp.status_code == 200
        assert resp.json()["project_id"] == "proj_new"

    def test_update_syncs_to_index(self, test_client: Any) -> None:
        create_resp = test_client.post("/chats", json={"title": "Old"})
        chat_id = create_resp.json()["id"]

        test_client.put(f"/chats/{chat_id}", json={"title": "Renamed"})

        index_resp = test_client.get("/chats")
        entry = next(c for c in index_resp.json()["chats"] if c["id"] == chat_id)
        assert entry["title"] == "Renamed"

    def test_update_nonexistent_chat(self, test_client: Any) -> None:
        resp = test_client.put(
            "/chats/chat_doesnotexist",
            json={"title": "Nope"},
        )
        assert resp.status_code == 404

    def test_update_preserves_messages(self, test_client: Any) -> None:
        """PUT should not modify the messages array."""
        create_resp = test_client.post("/chats", json={"title": "With Messages"})
        chat_id = create_resp.json()["id"]

        # Manually add a message via /chat
        with patch("runtime.app._ollama_list_models") as mock_list, \
             patch("runtime.app._ollama_chat") as mock_chat:
            mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
            mock_chat.return_value = {
                "content": "Hi there!",
                "model": "llama3",
                "tokens": 5,
                "duration_ms": 100,
            }
            with patch.object(
                test_client.app.state.cloud, "get_plan",
                side_effect=__import__("runtime.cloud_client", fromlist=["CloudUnavailable"]).CloudUnavailable("down"),
            ):
                test_client.post("/chat", json={"message": "hello", "chat_id": chat_id})

        # Now update title
        test_client.put(f"/chats/{chat_id}", json={"title": "Renamed"})

        # Messages should still be there
        get_resp = test_client.get(f"/chats/{chat_id}")
        data = get_resp.json()
        assert data["title"] == "Renamed"
        assert len(data["messages"]) == 2  # user + assistant


# ═══════════════════════════════════════════════════════════════════════
# 3. /chat Endpoint with Chat History Integration
# ═══════════════════════════════════════════════════════════════════════


class TestChatWithHistory:
    """Tests for POST /chat with chat_id integration."""

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_chat_creates_new_chat_when_no_chat_id(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """When no chat_id is provided, a new chat is created automatically."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "Hello!",
            "model": "llama3",
            "tokens": 10,
            "duration_ms": 150,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post("/chat", json={"message": "hi there"})

        assert resp.status_code == 200
        data = resp.json()
        assert "chat_id" in data
        assert data["chat_id"].startswith("chat_")
        assert data["content"] == "Hello!"

        # Verify chat was persisted
        chat_resp = test_client.get(f"/chats/{data['chat_id']}")
        chat = chat_resp.json()
        assert len(chat["messages"]) == 2
        assert chat["messages"][0]["role"] == "user"
        assert chat["messages"][0]["content"] == "hi there"
        assert chat["messages"][1]["role"] == "assistant"
        assert chat["messages"][1]["content"] == "Hello!"

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_chat_appends_to_existing_chat(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """When chat_id is provided, messages are appended to the existing chat."""
        from runtime.cloud_client import CloudUnavailable

        # Create a chat first
        create_resp = test_client.post("/chats", json={"title": "Ongoing"})
        chat_id = create_resp.json()["id"]

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "First response",
            "model": "llama3",
            "tokens": 10,
            "duration_ms": 100,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post(
                "/chat",
                json={"message": "first message", "chat_id": chat_id},
            )

        assert resp.status_code == 200
        assert resp.json()["chat_id"] == chat_id

        # Send a second message
        mock_chat.return_value = {
            "content": "Second response",
            "model": "llama3",
            "tokens": 10,
            "duration_ms": 100,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp2 = test_client.post(
                "/chat",
                json={"message": "second message", "chat_id": chat_id},
            )

        assert resp2.status_code == 200

        # Verify both exchanges are persisted
        chat_resp = test_client.get(f"/chats/{chat_id}")
        chat = chat_resp.json()
        assert len(chat["messages"]) == 4  # 2 user + 2 assistant
        assert chat["messages"][0]["content"] == "first message"
        assert chat["messages"][1]["content"] == "First response"
        assert chat["messages"][2]["content"] == "second message"
        assert chat["messages"][3]["content"] == "Second response"

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_chat_loads_history_from_chat_file(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """When chat_id is provided without explicit history, stored messages are used."""
        from runtime.cloud_client import CloudUnavailable

        create_resp = test_client.post("/chats", json={"title": "Memory Test"})
        chat_id = create_resp.json()["id"]

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "Response 1",
            "model": "llama3",
            "tokens": 5,
            "duration_ms": 50,
        }

        # First message
        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            test_client.post(
                "/chat",
                json={"message": "What is 2+2?", "chat_id": chat_id},
            )

        # Second message — should have first exchange as history
        mock_chat.return_value = {
            "content": "Response 2",
            "model": "llama3",
            "tokens": 5,
            "duration_ms": 50,
        }
        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            test_client.post(
                "/chat",
                json={"message": "And 3+3?", "chat_id": chat_id},
            )

        # Check that history was passed to _ollama_chat
        last_call = mock_chat.call_args
        history_arg = last_call.kwargs.get("history") or last_call[1].get("history")
        assert history_arg is not None
        # Should contain the first exchange (2 messages)
        assert len(history_arg) == 2
        assert history_arg[0]["role"] == "user"
        assert history_arg[0]["content"] == "What is 2+2?"
        assert history_arg[1]["role"] == "assistant"
        assert history_arg[1]["content"] == "Response 1"

    def test_chat_with_unknown_chat_id_returns_404(
        self, test_client: Any,
    ) -> None:
        """POST /chat with unknown chat_id returns 404."""
        resp = test_client.post(
            "/chat",
            json={"message": "hello", "chat_id": "chat_nonexistent"},
        )

        assert resp.status_code == 404
        assert "not found" in resp.json()["detail"].lower()

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_chat_auto_title_from_first_message(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """When chat has no title, first user message auto-generates one."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "It is a programming language.",
            "model": "llama3",
            "tokens": 10,
            "duration_ms": 100,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post(
                "/chat",
                json={"message": "What is Python?"},
            )

        chat_id = resp.json()["chat_id"]
        chat_resp = test_client.get(f"/chats/{chat_id}")
        assert chat_resp.json()["title"] == "What is Python?"

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_chat_does_not_overwrite_existing_title(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """If the chat already has a title, auto-title should not overwrite it."""
        from runtime.cloud_client import CloudUnavailable

        create_resp = test_client.post("/chats", json={"title": "My Custom Title"})
        chat_id = create_resp.json()["id"]

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "Sure!",
            "model": "llama3",
            "tokens": 5,
            "duration_ms": 50,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            test_client.post(
                "/chat",
                json={"message": "Ignore this for title", "chat_id": chat_id},
            )

        chat_resp = test_client.get(f"/chats/{chat_id}")
        assert chat_resp.json()["title"] == "My Custom Title"

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_chat_updates_index_last_message(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """After /chat, the index should reflect the last_message and updated_at."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "OK",
            "model": "llama3",
            "tokens": 3,
            "duration_ms": 50,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post(
                "/chat",
                json={"message": "Tell me about cats"},
            )

        chat_id = resp.json()["chat_id"]

        index_resp = test_client.get("/chats")
        entry = next(c for c in index_resp.json()["chats"] if c["id"] == chat_id)
        assert entry["last_message"] == "Tell me about cats"
        assert entry["title"] == "Tell me about cats"

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_chat_messages_have_timestamps(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """Messages should include timestamp fields."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "Hi!",
            "model": "llama3",
            "tokens": 2,
            "duration_ms": 30,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post("/chat", json={"message": "hello"})

        chat_id = resp.json()["chat_id"]
        chat_resp = test_client.get(f"/chats/{chat_id}")
        messages = chat_resp.json()["messages"]
        assert "timestamp" in messages[0]
        assert "timestamp" in messages[1]

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_chat_assistant_message_has_model_field(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """Assistant messages should include the model name."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "Hello!",
            "model": "llama3",
            "tokens": 5,
            "duration_ms": 50,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post("/chat", json={"message": "hey"})

        chat_id = resp.json()["chat_id"]
        chat_resp = test_client.get(f"/chats/{chat_id}")
        assistant_msg = chat_resp.json()["messages"][1]
        assert assistant_msg["model"] == "llama3"


# ═══════════════════════════════════════════════════════════════════════
# 4. Backward Compatibility
# ═══════════════════════════════════════════════════════════════════════


class TestChatBackwardCompatibility:
    """Ensure the /chat endpoint still works the same for callers not using chat_id."""

    def test_empty_message_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chat", json={"message": ""})
        assert resp.status_code == 400

    def test_missing_message_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chat", json={})
        assert resp.status_code == 400

    @patch("runtime.app._ollama_list_models", return_value=[])
    def test_no_models_available(self, _mock: MagicMock, test_client: Any) -> None:
        resp = test_client.post("/chat", json={"message": "hello"})
        assert resp.status_code == 503

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_response_still_has_content_and_model(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """Existing response fields (content, model, tokens, etc.) are preserved."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "Backward compat!",
            "model": "llama3",
            "tokens": 10,
            "duration_ms": 100,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post("/chat", json={"message": "hello"})

        data = resp.json()
        assert data["content"] == "Backward compat!"
        assert data["response"] == "Backward compat!"
        assert data["model"] == "llama3"
        assert data["tokens"] == 10
        assert data["duration_ms"] == 100
        assert data["source"] == "local_fallback"
        # New field
        assert "chat_id" in data


# ═══════════════════════════════════════════════════════════════════════
# 5. Edge Cases
# ═══════════════════════════════════════════════════════════════════════


class TestEdgeCases:
    """Edge cases for chat history."""

    def test_create_many_chats(self, test_client: Any) -> None:
        """Stress test: create 20 chats and verify all are listed."""
        for i in range(20):
            test_client.post("/chats", json={"title": f"Chat {i}"})

        resp = test_client.get("/chats")
        assert resp.json()["count"] == 20

    def test_delete_all_chats(self, test_client: Any) -> None:
        """Create and delete all chats."""
        ids = []
        for i in range(5):
            r = test_client.post("/chats", json={"title": f"Chat {i}"})
            ids.append(r.json()["id"])

        for cid in ids:
            test_client.delete(f"/chats/{cid}")

        resp = test_client.get("/chats")
        assert resp.json()["count"] == 0

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_auto_title_with_long_first_message(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """Auto-title should truncate to 50 chars + ellipsis."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "OK",
            "model": "llama3",
            "tokens": 2,
            "duration_ms": 30,
        }

        long_message = "This is a very long message that should be truncated when used as an auto-title for the chat"

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post("/chat", json={"message": long_message})

        chat_id = resp.json()["chat_id"]
        chat_resp = test_client.get(f"/chats/{chat_id}")
        title = chat_resp.json()["title"]
        assert len(title) <= 53  # 50 + "..."
        assert title.endswith("...")

    def test_chat_file_structure(self, test_client: Any, runtime_config: RuntimeConfig) -> None:
        """Verify the on-disk file structure matches the spec."""
        create_resp = test_client.post("/chats", json={"title": "Disk Check"})
        chat_id = create_resp.json()["id"]

        # Check file exists
        chat_file = runtime_config.data_dir / "chats" / f"{chat_id}.json"
        assert chat_file.exists()

        # Check index exists
        index_file = runtime_config.data_dir / "chats" / "index.json"
        assert index_file.exists()

        # Parse chat file
        chat_data = json.loads(chat_file.read_text("utf-8"))
        assert chat_data["id"] == chat_id
        assert chat_data["title"] == "Disk Check"
        assert isinstance(chat_data["messages"], list)

        # Parse index file
        index_data = json.loads(index_file.read_text("utf-8"))
        assert isinstance(index_data, list)
        assert any(e["id"] == chat_id for e in index_data)
