"""Tests for web chat module."""

from datetime import datetime

import pytest
from fastapi import WebSocketDisconnect

from oclawma.web.auth import ConnectionTracker, TokenAuth
from oclawma.web.chat_history import ChatHistory
from oclawma.web.models import ChatMessage, ChatRequest, MessageRole, WebSocketMessage


class TestChatMessage:
    """Test ChatMessage model."""

    def test_create_message(self):
        """Test creating a chat message."""
        msg = ChatMessage(
            id="test-id",
            role=MessageRole.USER,
            content="Hello",
        )
        assert msg.id == "test-id"
        assert msg.role == MessageRole.USER
        assert msg.content == "Hello"
        assert isinstance(msg.timestamp, datetime)
        assert msg.metadata == {}

    def test_message_to_dict(self):
        """Test message serialization."""
        msg = ChatMessage(
            id="test-id",
            role=MessageRole.ASSISTANT,
            content="Hi there",
            metadata={"key": "value"},
        )
        data = msg.model_dump()
        assert data["id"] == "test-id"
        assert data["role"] == "assistant"
        assert data["content"] == "Hi there"
        assert data["metadata"] == {"key": "value"}


class TestChatRequest:
    """Test ChatRequest model."""

    def test_valid_request(self):
        """Test valid chat request."""
        req = ChatRequest(content="Hello")
        assert req.content == "Hello"
        assert req.conversation_id is None

    def test_request_with_conversation(self):
        """Test request with conversation ID."""
        req = ChatRequest(content="Hello", conversation_id="conv-123")
        assert req.content == "Hello"
        assert req.conversation_id == "conv-123"

    def test_empty_content_rejected(self):
        """Test that empty content is rejected."""
        with pytest.raises(ValueError):
            ChatRequest(content="")

    def test_content_too_long(self):
        """Test that very long content is rejected."""
        with pytest.raises(ValueError):
            ChatRequest(content="x" * 10001)


class TestWebSocketMessage:
    """Test WebSocketMessage model."""

    def test_valid_message(self):
        """Test valid websocket message."""
        msg = WebSocketMessage(type="chat", data={"content": "Hello"})
        assert msg.type == "chat"
        assert msg.data == {"content": "Hello"}

    def test_from_json(self):
        """Test parsing from JSON."""
        json_str = '{"type": "ping", "data": {}}'
        msg = WebSocketMessage.model_validate_json(json_str)
        assert msg.type == "ping"
        assert msg.data == {}


class TestTokenAuth:
    """Test TokenAuth functionality."""

    def test_generates_random_token(self):
        """Test that random token is generated if not provided."""
        auth = TokenAuth()
        assert len(auth.get_token()) > 20

    def test_uses_provided_token(self):
        """Test that provided token is used."""
        auth = TokenAuth(token="my-secret-token")
        assert auth.get_token() == "my-secret-token"

    def test_regenerate_token(self):
        """Test token regeneration."""
        auth = TokenAuth(token="old-token")
        new_token = auth.regenerate_token()
        assert new_token != "old-token"
        assert auth.get_token() == new_token

    @pytest.mark.asyncio
    async def test_validates_demo_token(self):
        """Test that demo token is accepted."""
        auth = TokenAuth()

        # Create mock credentials
        class MockCreds:
            scheme = "Bearer"
            credentials = "demo"

        result = await auth(MockCreds())
        assert result == "demo"

    @pytest.mark.asyncio
    async def test_rejects_invalid_token(self):
        """Test that invalid token is rejected."""
        auth = TokenAuth(token="valid-token")

        class MockCreds:
            scheme = "Bearer"
            credentials = "invalid-token"

        with pytest.raises(Exception) as exc_info:
            await auth(MockCreds())
        assert "Invalid token" in str(exc_info.value)


class TestConnectionTracker:
    """Test ConnectionTracker functionality."""

    def test_add_connection(self):
        """Test adding a connection."""
        tracker = ConnectionTracker()
        tracker.add_connection("conn-1", {"client": "test"})
        assert tracker.get_connection_count() == 1

    def test_remove_connection(self):
        """Test removing a connection."""
        tracker = ConnectionTracker()
        tracker.add_connection("conn-1")
        tracker.remove_connection("conn-1")
        assert tracker.get_connection_count() == 0

    def test_get_connection_info(self):
        """Test getting connection info."""
        tracker = ConnectionTracker()
        tracker.add_connection("conn-1", {"client": "test"})
        info = tracker.get_connection_info("conn-1")
        assert info is not None
        assert info["client_info"] == {"client": "test"}

    def test_get_nonexistent_connection(self):
        """Test getting info for nonexistent connection."""
        tracker = ConnectionTracker()
        info = tracker.get_connection_info("nonexistent")
        assert info is None


class TestChatHistory:
    """Test ChatHistory persistence."""

    @pytest.fixture
    def history(self, tmp_path):
        """Create temporary ChatHistory instance."""
        db_path = tmp_path / "test.db"
        return ChatHistory(db_path)

    def test_create_conversation(self, history):
        """Test creating a conversation."""
        conv_id = history.create_conversation("Test Chat")
        assert conv_id is not None
        assert len(conv_id) > 0

        conv = history.get_conversation(conv_id)
        assert conv is not None
        assert conv["title"] == "Test Chat"

    def test_list_conversations(self, history):
        """Test listing conversations."""
        history.create_conversation("Chat 1")
        history.create_conversation("Chat 2")

        conversations = history.list_conversations()
        assert len(conversations) == 2

    def test_add_message(self, history):
        """Test adding a message."""
        conv_id = history.create_conversation()
        msg = history.add_message(
            conversation_id=conv_id,
            role=MessageRole.USER,
            content="Hello",
        )

        assert msg.role == MessageRole.USER
        assert msg.content == "Hello"
        assert msg.id is not None

    def test_get_messages(self, history):
        """Test retrieving messages."""
        conv_id = history.create_conversation()

        history.add_message(conv_id, MessageRole.USER, "Hello")
        history.add_message(conv_id, MessageRole.ASSISTANT, "Hi there")

        messages = history.get_messages(conv_id)
        assert len(messages) == 2
        assert messages[0].role == MessageRole.USER
        assert messages[1].role == MessageRole.ASSISTANT

    def test_add_message_to_nonexistent_conversation(self, history):
        """Test adding message to nonexistent conversation."""
        with pytest.raises(ValueError) as exc_info:
            history.add_message("nonexistent", MessageRole.USER, "Hello")
        assert "not found" in str(exc_info.value)

    def test_delete_conversation(self, history):
        """Test deleting a conversation."""
        conv_id = history.create_conversation()
        history.add_message(conv_id, MessageRole.USER, "Hello")

        deleted = history.delete_conversation(conv_id)
        assert deleted is True

        conv = history.get_conversation(conv_id)
        assert conv is None

    def test_delete_nonexistent_conversation(self, history):
        """Test deleting nonexistent conversation."""
        deleted = history.delete_conversation("nonexistent")
        assert deleted is False

    def test_clear_conversation(self, history):
        """Test clearing conversation messages."""
        conv_id = history.create_conversation()
        history.add_message(conv_id, MessageRole.USER, "Hello")

        cleared = history.clear_conversation(conv_id)
        assert cleared is True

        messages = history.get_messages(conv_id)
        assert len(messages) == 0

    def test_update_conversation_title(self, history):
        """Test updating conversation title."""
        conv_id = history.create_conversation("Old Title")

        updated = history.update_conversation_title(conv_id, "New Title")
        assert updated is True

        conv = history.get_conversation(conv_id)
        assert conv["title"] == "New Title"


class TestServerIntegration:
    """Integration tests for FastAPI server."""

    @pytest.fixture
    def app(self, tmp_path, monkeypatch):
        """Create test app with temp data dir."""
        pytest.importorskip("fastapi")
        pytest.importorskip("starlette")

        from oclawma.web import server

        # Patch get_data_dir to use temp path
        monkeypatch.setattr(server, "get_data_dir", lambda: tmp_path)

        from oclawma.web.server import create_app

        return create_app(auth_token="test-token", behind_proxy=False)

    @pytest.fixture
    def client(self, app):
        """Create test client."""
        from fastapi.testclient import TestClient

        with TestClient(app) as client:
            yield client

    def test_health_endpoint(self, client):
        """Test health check endpoint."""
        response = client.get("/health")
        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "healthy"
        assert "active_connections" in data

    def test_root_redirects_to_static(self, client):
        """Test root endpoint serves HTML."""
        response = client.get("/")
        assert response.status_code == 200
        assert "text/html" in response.headers["content-type"]

    def test_list_conversations_requires_auth(self, client):
        """Test conversations endpoint requires auth."""
        response = client.get("/api/conversations")
        assert response.status_code == 401

    def test_list_conversations_with_auth(self, client):
        """Test listing conversations with auth."""
        response = client.get(
            "/api/conversations",
            headers={"Authorization": "Bearer test-token"},
        )
        assert response.status_code == 200
        assert response.json() == []

    def test_create_conversation(self, client):
        """Test creating a conversation."""
        response = client.post(
            "/api/conversations",
            headers={"Authorization": "Bearer test-token"},
        )
        assert response.status_code == 201
        data = response.json()
        assert "id" in data

    def test_get_conversation(self, client):
        """Test getting a conversation."""
        # Create first
        create_resp = client.post(
            "/api/conversations",
            headers={"Authorization": "Bearer test-token"},
        )
        conv_id = create_resp.json()["id"]

        # Get
        response = client.get(
            f"/api/conversations/{conv_id}",
            headers={"Authorization": "Bearer test-token"},
        )
        assert response.status_code == 200
        assert response.json()["id"] == conv_id

    def test_get_nonexistent_conversation(self, client):
        """Test getting nonexistent conversation."""
        response = client.get(
            "/api/conversations/nonexistent",
            headers={"Authorization": "Bearer test-token"},
        )
        assert response.status_code == 404

    def test_chat_endpoint(self, client):
        """Test chat endpoint."""
        response = client.post(
            "/api/chat",
            headers={"Authorization": "Bearer test-token"},
            json={"content": "Hello"},
        )
        assert response.status_code == 200
        data = response.json()
        assert "message" in data
        assert "conversation_id" in data

    def test_chat_with_existing_conversation(self, client):
        """Test chat with existing conversation."""
        # Create conversation
        create_resp = client.post(
            "/api/conversations",
            headers={"Authorization": "Bearer test-token"},
        )
        conv_id = create_resp.json()["id"]

        # Chat in that conversation
        response = client.post(
            "/api/chat",
            headers={"Authorization": "Bearer test-token"},
            json={"content": "Hello", "conversation_id": conv_id},
        )
        assert response.status_code == 200
        assert response.json()["conversation_id"] == conv_id

    def test_delete_conversation(self, client):
        """Test deleting a conversation."""
        # Create
        create_resp = client.post(
            "/api/conversations",
            headers={"Authorization": "Bearer test-token"},
        )
        conv_id = create_resp.json()["id"]

        # Delete
        response = client.delete(
            f"/api/conversations/{conv_id}",
            headers={"Authorization": "Bearer test-token"},
        )
        assert response.status_code == 200
        assert response.json()["deleted"] is True

    @pytest.mark.asyncio
    async def test_websocket_connection(self, client):
        """Test WebSocket connection."""
        with client.websocket_connect("/ws?token=test-token") as websocket:
            # Receive connected message
            data = websocket.receive_json()
            assert data["type"] == "connected"

    @pytest.mark.asyncio
    async def test_websocket_invalid_token(self, client):
        """Test WebSocket with invalid token."""
        with pytest.raises(WebSocketDisconnect), client.websocket_connect("/ws?token=invalid"):
            pass

    @pytest.mark.asyncio
    async def test_websocket_chat(self, client):
        """Test WebSocket chat."""
        with client.websocket_connect("/ws?token=test-token") as websocket:
            # Skip connected message
            websocket.receive_json()

            # Send chat message
            websocket.send_json(
                {
                    "type": "chat",
                    "data": {"content": "Hello WebSocket"},
                }
            )

            # Receive user message acknowledgment
            msg1 = websocket.receive_json()
            assert msg1["type"] == "message_received"

            # Receive typing indicator(s) - may be multiple
            msg = websocket.receive_json()
            while msg["type"] == "typing":
                msg = websocket.receive_json()

            # Receive response (after typing indicators)
            assert msg["type"] == "message"
            assert "content" in msg["data"]["message"]

    @pytest.mark.asyncio
    async def test_websocket_ping(self, client):
        """Test WebSocket ping/pong."""
        with client.websocket_connect("/ws?token=test-token") as websocket:
            websocket.receive_json()  # Skip connected

            websocket.send_json({"type": "ping", "data": {}})
            response = websocket.receive_json()
            assert response["type"] == "pong"
