"""
Comprehensive unit tests for App models.

This test suite covers:
- App configuration validation
- App-Message relationships
- Conversation model integrity
- Annotation model relationships
"""

import json
from datetime import UTC, datetime
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import MagicMock, PropertyMock, patch
from uuid import uuid4

import pytest
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import Session

from models.enums import ConversationFromSource
from models.model import (
    App,
    AppAnnotationHitHistory,
    AppAnnotationSetting,
    AppMode,
    AppModelConfig,
    Conversation,
    IconType,
    Message,
    MessageAnnotation,
    Site,
    load_annotation_reply_config,
)


class TestAppModelValidation:
    """Test suite for App model validation and basic operations."""

    def test_app_creation_with_required_fields(self):
        """Test creating an app with all required fields."""
        # Arrange
        tenant_id = str(uuid4())
        created_by = str(uuid4())

        # Act
        app = App(
            tenant_id=tenant_id,
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=False,
            created_by=created_by,
        )

        # Assert
        assert app.name == "Test App"
        assert app.tenant_id == tenant_id
        assert app.mode == AppMode.CHAT
        assert app.enable_site is True
        assert app.enable_api is False
        assert app.created_by == created_by

    def test_app_creation_with_optional_fields(self):
        """Test creating an app with optional fields."""
        # Arrange & Act
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.COMPLETION,
            enable_site=True,
            enable_api=True,
            created_by=str(uuid4()),
            description="Test description",
            icon_type=IconType.EMOJI,
            icon="🤖",
            icon_background="#FF5733",
            is_demo=True,
            is_public=False,
            api_rpm=100,
            api_rph=1000,
        )

        # Assert
        assert app.description == "Test description"
        assert app.icon_type == IconType.EMOJI
        assert app.icon == "🤖"
        assert app.icon_background == "#FF5733"
        assert app.is_demo is True
        assert app.is_public is False
        assert app.api_rpm == 100
        assert app.api_rph == 1000

    def test_app_mode_validation(self):
        """Test app mode enum values."""
        # Assert
        expected_modes = {
            "chat",
            "completion",
            "workflow",
            "advanced-chat",
            "agent-chat",
            "agent",
            "channel",
            "rag-pipeline",
        }
        assert {mode.value for mode in AppMode} == expected_modes

    def test_app_mode_value_of(self):
        """Test AppMode.value_of method."""
        # Act & Assert
        assert AppMode.value_of("chat") == AppMode.CHAT
        assert AppMode.value_of("completion") == AppMode.COMPLETION
        assert AppMode.value_of("workflow") == AppMode.WORKFLOW

        with pytest.raises(ValueError, match="invalid mode value"):
            AppMode.value_of("invalid_mode")

    def test_icon_type_validation(self):
        """Test icon type enum values."""
        # Assert
        assert {t.value for t in IconType} == {"image", "emoji", "link"}

    def test_app_desc_or_prompt_with_description(self):
        """Test desc_or_prompt property when description exists."""
        # Arrange
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=False,
            created_by=str(uuid4()),
            description="App description",
        )

        # Act
        result = app.desc_or_prompt

        # Assert
        assert result == "App description"

    def test_app_desc_or_prompt_without_description(self):
        """Test desc_or_prompt property when description is empty."""
        # Arrange
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=False,
            created_by=str(uuid4()),
            description="",
        )

        session = MagicMock()
        with patch.object(App, "app_model_config_with_session", return_value=None) as get_model_config:
            # Act
            result = app.desc_or_prompt_with_session(session=session)

            # Assert
            assert result == ""
            get_model_config.assert_called_once_with(session=session)

    def test_app_is_agent_property_false(self):
        """Test is_agent property returns False when not configured as agent."""
        # Arrange
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=False,
            created_by=str(uuid4()),
        )

        with patch("models.model.db") as mock_db:
            # Act
            result = app.is_agent

            # Assert
            assert result is False
            mock_db.session.assert_called_once_with()

    def test_app_is_agent_with_session_updates_legacy_agent_mode(self):
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=False,
            created_by=str(uuid4()),
        )
        app.app_model_config_id = "config-1"
        app_model_config = MagicMock(spec=AppModelConfig)
        app_model_config.agent_mode = "agent"
        app_model_config.agent_mode_dict = {"enabled": True, "strategy": "react"}
        session = MagicMock()
        session.get.return_value = app_model_config

        result = app.is_agent_with_session(session=session)

        assert result is True
        assert app.mode == AppMode.AGENT_CHAT
        session.get.assert_called_once_with(AppModelConfig, "config-1")
        session.execute.assert_called_once()
        session.commit.assert_called_once_with()

    @pytest.mark.parametrize("sqlite_session", [(App, AppModelConfig)], indirect=True)
    def test_app_is_agent_with_session_persists_mode_across_sessions(self, sqlite_session: Session):
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=False,
            created_by=str(uuid4()),
        )
        sqlite_session.add(app)
        sqlite_session.flush()
        model_config = AppModelConfig(
            app_id=app.id,
            agent_mode=json.dumps({"enabled": True, "strategy": "react"}),
        )
        sqlite_session.add(model_config)
        sqlite_session.flush()
        app.app_model_config_id = model_config.id
        sqlite_session.commit()

        with Session(sqlite_session.get_bind(), expire_on_commit=False) as migration_session:
            assert app.is_agent_with_session(session=migration_session) is True

        sqlite_session.expire_all()
        assert sqlite_session.get(App, app.id).mode == AppMode.AGENT_CHAT

    def test_app_mode_compatible_with_agent(self):
        """Test mode_compatible_with_agent property."""
        # Arrange
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=False,
            created_by=str(uuid4()),
        )

        session = MagicMock()
        with patch.object(App, "is_agent_with_session", return_value=False) as is_agent:
            # Act
            result = app.mode_compatible_with_agent_with_session(session=session)

            # Assert
            assert result == AppMode.CHAT
            is_agent.assert_called_once_with(session=session)

    def test_deleted_tools_checks_plugin_builtin_providers_through_core_plugin_service(self):
        """Plugin-backed built-in tools are checked through core PluginService."""
        # Arrange
        app = App(
            tenant_id="tenant-1",
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=False,
            created_by=str(uuid4()),
        )
        app_model_config = AppModelConfig(
            app_id=str(uuid4()),
            agent_mode=json.dumps(
                {
                    "enabled": True,
                    "strategy": "function_call",
                    "tools": [
                        {
                            "provider_type": "builtin",
                            "provider_id": "langgenius/openai/openai",
                            "tool_name": "chat",
                            "tool_parameters": {},
                        }
                    ],
                    "prompt": None,
                }
            ),
        )
        session = MagicMock()

        # Act
        with (
            patch.object(App, "app_model_config_with_session", return_value=app_model_config) as get_model_config,
            patch("core.tools.tool_manager.ToolManager.get_hardcoded_provider", side_effect=Exception),
            patch("core.plugin.plugin_service.PluginService.check_tools_existence", return_value=[False]) as exists,
        ):
            result = app.deleted_tools_with_session(session=session)

        # Assert
        assert result == [{"type": "builtin", "tool_name": "chat", "provider_id": "langgenius/openai/openai"}]
        get_model_config.assert_called_once_with(session=session)
        exists.assert_called_once()
        assert exists.call_args.args[0] == "tenant-1"
        assert [str(provider_id) for provider_id in exists.call_args.args[1]] == ["langgenius/openai/openai"]


class TestAppModelConfig:
    """Test suite for AppModelConfig model."""

    def test_app_model_config_creation(self):
        """Test creating an AppModelConfig."""
        # Arrange
        app_id = str(uuid4())
        created_by = str(uuid4())

        # Act
        config = AppModelConfig(
            app_id=app_id,
            provider="openai",
            model_id="gpt-4",
            created_by=created_by,
        )

        # Assert
        assert config.app_id == app_id
        assert config.provider == "openai"
        assert config.model_id == "gpt-4"
        assert config.created_by == created_by

    def test_app_model_config_with_configs_json(self):
        """Test AppModelConfig with JSON configs."""
        # Arrange
        configs = {"temperature": 0.7, "max_tokens": 1000}

        # Act
        config = AppModelConfig(
            app_id=str(uuid4()),
            provider="openai",
            model_id="gpt-4",
            created_by=str(uuid4()),
            configs=configs,
        )

        # Assert
        assert config.configs == configs

    def test_app_model_config_model_dict_property(self):
        """Test model_dict property."""
        # Arrange
        model_data = {"provider": "openai", "name": "gpt-4"}
        config = AppModelConfig(
            app_id=str(uuid4()),
            provider="openai",
            model_id="gpt-4",
            created_by=str(uuid4()),
            model=json.dumps(model_data),
        )

        # Act
        result = config.model_dict

        # Assert
        assert result == model_data

    def test_app_model_config_model_dict_empty(self):
        """Test model_dict property when model is None."""
        # Arrange
        config = AppModelConfig(
            app_id=str(uuid4()),
            provider="openai",
            model_id="gpt-4",
            created_by=str(uuid4()),
            model=None,
        )

        # Act
        result = config.model_dict

        # Assert
        assert result == {}

    def test_app_model_config_suggested_questions_list(self):
        """Test suggested_questions_list property."""
        # Arrange
        questions = ["What can you do?", "How does this work?"]
        config = AppModelConfig(
            app_id=str(uuid4()),
            provider="openai",
            model_id="gpt-4",
            created_by=str(uuid4()),
            suggested_questions=json.dumps(questions),
        )

        # Act
        result = config.suggested_questions_list

        # Assert
        assert result == questions

    def test_to_dict_uses_injected_annotation_reply(self):
        config = AppModelConfig(app_id=str(uuid4()))
        annotation_reply = {"enabled": False}

        with patch.object(
            AppModelConfig,
            "annotation_reply_dict",
            new_callable=PropertyMock,
            side_effect=AssertionError("annotation_reply_dict should not be accessed"),
        ):
            result = config.to_dict(annotation_reply=annotation_reply)

        assert result["annotation_reply"] == annotation_reply


class TestAnnotationReplyConfigLoader:
    def test_load_annotation_reply_config_returns_disabled_when_setting_missing(self):
        session = MagicMock()
        session.scalar.return_value = None

        result = load_annotation_reply_config(session, "app-1")

        assert result == {"enabled": False}
        session.scalar.assert_called_once()
        stmt = session.scalar.call_args.args[0]
        compiled = str(stmt.compile(dialect=postgresql.dialect()))
        assert "app_annotation_settings.app_id" in compiled
        assert stmt.compile().params == {"app_id_1": "app-1"}

    def test_load_annotation_reply_config_returns_embedding_model(self):
        session = MagicMock()
        annotation_setting = SimpleNamespace(
            id="annotation-1",
            score_threshold=0.7,
            collection_binding_id="binding-1",
        )
        collection_binding = SimpleNamespace(provider_name="provider", model_name="embedding")
        session.scalar.side_effect = [annotation_setting, collection_binding]

        result = load_annotation_reply_config(session, "app-1")

        assert result == {
            "id": "annotation-1",
            "enabled": True,
            "score_threshold": 0.7,
            "embedding_model": {
                "embedding_provider_name": "provider",
                "embedding_model_name": "embedding",
            },
        }
        assert session.scalar.call_count == 2
        stmt = session.scalar.call_args_list[1].args[0]
        compiled = str(stmt.compile(dialect=postgresql.dialect()))
        assert "dataset_collection_bindings.id" in compiled
        assert stmt.compile().params == {"id_1": "binding-1"}

    def test_load_annotation_reply_config_raises_when_binding_missing(self):
        session = MagicMock()
        annotation_setting = SimpleNamespace(collection_binding_id="binding-1")
        session.scalar.side_effect = [annotation_setting, None]

        with pytest.raises(ValueError, match="Collection binding detail not found"):
            load_annotation_reply_config(session, "app-1")


class TestConversationModel:
    """Test suite for Conversation model integrity."""

    def test_conversation_creation_with_required_fields(self):
        """Test creating a conversation with required fields."""
        # Arrange
        app_id = str(uuid4())
        from_end_user_id = str(uuid4())

        # Act
        conversation = Conversation(
            app_id=app_id,
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=from_end_user_id,
        )

        # Assert
        assert conversation.app_id == app_id
        assert conversation.mode == AppMode.CHAT
        assert conversation.name == "Test Conversation"
        assert conversation.status == "normal"
        assert conversation.from_source == "api"
        assert conversation.from_end_user_id == from_end_user_id

    def test_conversation_with_inputs(self):
        """Test conversation inputs property."""
        # Arrange
        inputs = {"query": "Hello", "context": "test"}
        conversation = Conversation(
            app_id=str(uuid4()),
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=str(uuid4()),
        )
        conversation._inputs = inputs

        # Act
        result = conversation.inputs

        # Assert
        assert result == inputs

    def test_conversation_inputs_setter(self):
        """Test conversation inputs setter."""
        # Arrange
        conversation = Conversation(
            app_id=str(uuid4()),
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=str(uuid4()),
        )
        inputs = {"query": "Hello", "context": "test"}

        # Act
        conversation.inputs = inputs

        # Assert
        assert conversation._inputs == inputs

    def test_conversation_summary_or_query_with_summary(self):
        """Test summary_or_query property when summary exists."""
        # Arrange
        conversation = Conversation(
            app_id=str(uuid4()),
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=str(uuid4()),
            summary="Test summary",
        )

        # Act
        result = conversation.summary_or_query

        # Assert
        assert result == "Test summary"

    def test_conversation_summary_or_query_without_summary(self):
        """Test summary_or_query property when summary is empty."""
        # Arrange
        conversation = Conversation(
            app_id=str(uuid4()),
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=str(uuid4()),
            summary=None,
        )

        # Mock first_message to return a message with query
        mock_message = MagicMock()
        mock_message.query = "First message query"
        session = MagicMock()
        with patch.object(Conversation, "first_message_with_session", return_value=mock_message) as get_first_message:
            # Act
            result = conversation.summary_or_query_with_session(session=session)

            # Assert
            assert result == "First message query"
            get_first_message.assert_called_once_with(session=session)

    def test_model_config_uses_caller_session_for_annotation_reply(self):
        conversation = Conversation(
            app_id="app-1",
            app_model_config_id="config-1",
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=str(uuid4()),
            model_id="model-1",
            model_provider="provider-1",
        )
        app_model_config = MagicMock(spec=AppModelConfig)
        app_model_config.app_id = "app-1"
        app_model_config.to_dict.return_value = {}
        session = MagicMock()
        session.scalar.return_value = app_model_config
        annotation_reply = {"enabled": False}

        with patch("models.model.load_annotation_reply_config", return_value=annotation_reply) as load_config:
            result = conversation.model_config_with_session(session=session)

        load_config.assert_called_once_with(session, "app-1")
        app_model_config.to_dict.assert_called_once_with(annotation_reply=annotation_reply)
        assert result["model_id"] == "model-1"
        assert result["provider"] == "provider-1"

    def test_override_model_config_uses_caller_session_for_annotation_reply(self):
        conversation = Conversation(
            app_id="app-1",
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=str(uuid4()),
            override_model_configs=json.dumps({"model": {}}),
        )
        app_model_config = MagicMock(spec=AppModelConfig)
        app_model_config.app_id = "app-1"
        app_model_config.to_dict.return_value = {}
        session = MagicMock()
        annotation_reply = {"enabled": False}

        with (
            patch.object(AppModelConfig, "from_model_config_dict", return_value=app_model_config),
            patch("models.model.load_annotation_reply_config", return_value=annotation_reply) as load_config,
        ):
            conversation.model_config_with_session(session=session)

        load_config.assert_called_once_with(session, "app-1")
        app_model_config.to_dict.assert_called_once_with(annotation_reply=annotation_reply)
        session.scalar.assert_not_called()

    def test_conversation_in_debug_mode(self):
        """Test in_debug_mode property."""
        # Arrange
        conversation = Conversation(
            app_id=str(uuid4()),
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=str(uuid4()),
            override_model_configs='{"model": "gpt-4"}',
        )

        # Act
        result = conversation.in_debug_mode

        # Assert
        assert result is True

    def test_conversation_to_dict_serialization(self):
        """Test conversation to_dict method."""
        # Arrange
        app_id = str(uuid4())
        from_end_user_id = str(uuid4())
        conversation = Conversation(
            app_id=app_id,
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=from_end_user_id,
            dialogue_count=5,
        )
        conversation.id = str(uuid4())
        conversation._inputs = {"query": "test"}

        # Act
        result = conversation.to_dict()

        # Assert
        assert result["id"] == conversation.id
        assert result["app_id"] == app_id
        assert result["mode"] == AppMode.CHAT
        assert result["name"] == "Test Conversation"
        assert result["status"] == "normal"
        assert result["from_source"] == "api"
        assert result["from_end_user_id"] == from_end_user_id
        assert result["dialogue_count"] == 5
        assert result["inputs"] == {"query": "test"}


class TestMessageModel:
    """Test suite for Message model and App-Message relationships."""

    def test_message_creation_with_required_fields(self):
        """Test creating a message with required fields."""
        # Arrange
        app_id = str(uuid4())
        conversation_id = str(uuid4())

        # Act
        message = Message(
            app_id=app_id,
            conversation_id=conversation_id,
            query="What is AI?",
            message={"role": "user", "content": "What is AI?"},
            answer="AI stands for Artificial Intelligence.",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            currency="USD",
            from_source=ConversationFromSource.API,
        )

        # Assert
        assert message.app_id == app_id
        assert message.conversation_id == conversation_id
        assert message.query == "What is AI?"
        assert message.answer == "AI stands for Artificial Intelligence."
        assert message.currency == "USD"
        assert message.from_source == "api"

    def test_message_with_inputs(self):
        """Test message inputs property."""
        # Arrange
        inputs = {"query": "Hello", "context": "test"}
        message = Message(
            app_id=str(uuid4()),
            conversation_id=str(uuid4()),
            query="Test query",
            message={"role": "user", "content": "Test"},
            answer="Test answer",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            currency="USD",
            from_source=ConversationFromSource.API,
        )
        message._inputs = inputs

        # Act
        result = message.inputs

        # Assert
        assert result == inputs

    def test_message_inputs_setter(self):
        """Test message inputs setter."""
        # Arrange
        message = Message(
            app_id=str(uuid4()),
            conversation_id=str(uuid4()),
            query="Test query",
            message={"role": "user", "content": "Test"},
            answer="Test answer",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            currency="USD",
            from_source=ConversationFromSource.API,
        )
        inputs = {"query": "Hello", "context": "test"}

        # Act
        message.inputs = inputs

        # Assert
        assert message._inputs == inputs

    def test_message_in_debug_mode(self):
        """Test message in_debug_mode property."""
        # Arrange
        message = Message(
            app_id=str(uuid4()),
            conversation_id=str(uuid4()),
            query="Test query",
            message={"role": "user", "content": "Test"},
            answer="Test answer",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            currency="USD",
            from_source=ConversationFromSource.API,
            override_model_configs='{"model": "gpt-4"}',
        )

        # Act
        result = message.in_debug_mode

        # Assert
        assert result is True

    def test_message_metadata_dict_property(self):
        """Test message_metadata_dict property."""
        # Arrange
        metadata = {"retriever_resources": ["doc1", "doc2"], "usage": {"tokens": 100}}
        message = Message(
            app_id=str(uuid4()),
            conversation_id=str(uuid4()),
            query="Test query",
            message={"role": "user", "content": "Test"},
            answer="Test answer",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            currency="USD",
            from_source=ConversationFromSource.API,
            message_metadata=json.dumps(metadata),
        )

        # Act
        result = message.message_metadata_dict

        # Assert
        assert result == metadata

    def test_message_metadata_dict_empty(self):
        """Test message_metadata_dict when metadata is None."""
        # Arrange
        message = Message(
            app_id=str(uuid4()),
            conversation_id=str(uuid4()),
            query="Test query",
            message={"role": "user", "content": "Test"},
            answer="Test answer",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            currency="USD",
            from_source=ConversationFromSource.API,
            message_metadata=None,
        )

        # Act
        result = message.message_metadata_dict

        # Assert
        assert result == {}

    def test_message_to_dict_serialization(self):
        """Test message to_dict method."""
        # Arrange
        app_id = str(uuid4())
        conversation_id = str(uuid4())
        now = datetime.now(UTC)

        message = Message(
            app_id=app_id,
            conversation_id=conversation_id,
            query="Test query",
            message={"role": "user", "content": "Test"},
            answer="Test answer",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            total_price=Decimal("0.0003"),
            currency="USD",
            from_source=ConversationFromSource.API,
            status="normal",
        )
        message.id = str(uuid4())
        message._inputs = {"query": "test"}
        message.created_at = now
        message.updated_at = now

        # Act
        result = message.to_dict()

        # Assert
        assert result["id"] == message.id
        assert result["app_id"] == app_id
        assert result["conversation_id"] == conversation_id
        assert result["query"] == "Test query"
        assert result["answer"] == "Test answer"
        assert result["status"] == "normal"
        assert result["from_source"] == "api"
        assert result["inputs"] == {"query": "test"}
        assert "created_at" in result
        assert "updated_at" in result

    def test_message_from_dict_deserialization(self):
        """Test message from_dict method."""
        # Arrange
        message_id = str(uuid4())
        app_id = str(uuid4())
        conversation_id = str(uuid4())
        data = {
            "id": message_id,
            "app_id": app_id,
            "conversation_id": conversation_id,
            "model_id": "gpt-4",
            "inputs": {"query": "test"},
            "query": "Test query",
            "message": {"role": "user", "content": "Test"},
            "answer": "Test answer",
            "total_price": Decimal("0.0003"),
            "status": "normal",
            "error": None,
            "message_metadata": {"usage": {"tokens": 100}},
            "from_source": "api",
            "from_end_user_id": None,
            "from_account_id": None,
            "created_at": "2024-01-01T00:00:00",
            "updated_at": "2024-01-01T00:00:00",
            "agent_based": False,
            "workflow_run_id": None,
        }

        # Act
        message = Message.from_dict(data)

        # Assert
        assert message.id == message_id
        assert message.app_id == app_id
        assert message.conversation_id == conversation_id
        assert message.query == "Test query"
        assert message.answer == "Test answer"


class TestMessageAnnotation:
    """Test suite for MessageAnnotation and annotation relationships."""

    def test_message_annotation_creation(self):
        """Test creating a message annotation."""
        # Arrange
        app_id = str(uuid4())
        conversation_id = str(uuid4())
        message_id = str(uuid4())
        account_id = str(uuid4())

        # Act
        annotation = MessageAnnotation(
            app_id=app_id,
            conversation_id=conversation_id,
            message_id=message_id,
            question="What is AI?",
            content="AI stands for Artificial Intelligence.",
            account_id=account_id,
        )

        # Assert
        assert annotation.app_id == app_id
        assert annotation.conversation_id == conversation_id
        assert annotation.message_id == message_id
        assert annotation.question == "What is AI?"
        assert annotation.content == "AI stands for Artificial Intelligence."
        assert annotation.account_id == account_id

    def test_message_annotation_without_message_id(self):
        """Test creating annotation without message_id."""
        # Arrange
        app_id = str(uuid4())
        account_id = str(uuid4())

        # Act
        annotation = MessageAnnotation(
            app_id=app_id,
            question="What is AI?",
            conversation_id=None,
            message_id=None,
            content="AI stands for Artificial Intelligence.",
            account_id=account_id,
        )

        # Assert
        assert annotation.app_id == app_id
        assert annotation.message_id is None
        assert annotation.conversation_id is None
        assert annotation.question == "What is AI?"
        assert annotation.content == "AI stands for Artificial Intelligence."

    def test_message_annotation_hit_count_default(self):
        """Test annotation hit_count default value."""
        # Arrange
        annotation = MessageAnnotation(
            app_id=str(uuid4()),
            question="Test question",
            conversation_id=None,
            message_id=None,
            content="Test content",
            account_id=str(uuid4()),
        )

        # Act & Assert - default value is set by database
        # Model instantiation doesn't set server defaults
        assert hasattr(annotation, "hit_count")


class TestAppAnnotationSetting:
    """Test suite for AppAnnotationSetting model."""

    def test_app_annotation_setting_creation(self):
        """Test creating an app annotation setting."""
        # Arrange
        app_id = str(uuid4())
        collection_binding_id = str(uuid4())
        created_user_id = str(uuid4())
        updated_user_id = str(uuid4())

        # Act
        setting = AppAnnotationSetting(
            app_id=app_id,
            score_threshold=0.8,
            collection_binding_id=collection_binding_id,
            created_user_id=created_user_id,
            updated_user_id=updated_user_id,
        )

        # Assert
        assert setting.app_id == app_id
        assert setting.score_threshold == 0.8
        assert setting.collection_binding_id == collection_binding_id
        assert setting.created_user_id == created_user_id
        assert setting.updated_user_id == updated_user_id

    def test_app_annotation_setting_score_threshold_validation(self):
        """Test score threshold values."""
        # Arrange & Act
        setting_high = AppAnnotationSetting(
            app_id=str(uuid4()),
            score_threshold=0.95,
            collection_binding_id=str(uuid4()),
            created_user_id=str(uuid4()),
            updated_user_id=str(uuid4()),
        )
        setting_low = AppAnnotationSetting(
            app_id=str(uuid4()),
            score_threshold=0.5,
            collection_binding_id=str(uuid4()),
            created_user_id=str(uuid4()),
            updated_user_id=str(uuid4()),
        )

        # Assert
        assert setting_high.score_threshold == 0.95
        assert setting_low.score_threshold == 0.5


class TestAppAnnotationHitHistory:
    """Test suite for AppAnnotationHitHistory model."""

    def test_app_annotation_hit_history_creation(self):
        """Test creating an annotation hit history."""
        # Arrange
        app_id = str(uuid4())
        annotation_id = str(uuid4())
        message_id = str(uuid4())
        account_id = str(uuid4())

        # Act
        history = AppAnnotationHitHistory(
            app_id=app_id,
            annotation_id=annotation_id,
            source="api",
            question="What is AI?",
            account_id=account_id,
            score=0.95,
            message_id=message_id,
            annotation_question="What is AI?",
            annotation_content="AI stands for Artificial Intelligence.",
        )

        # Assert
        assert history.app_id == app_id
        assert history.annotation_id == annotation_id
        assert history.source == "api"
        assert history.question == "What is AI?"
        assert history.account_id == account_id
        assert history.score == 0.95
        assert history.message_id == message_id
        assert history.annotation_question == "What is AI?"
        assert history.annotation_content == "AI stands for Artificial Intelligence."

    def test_app_annotation_hit_history_score_values(self):
        """Test annotation hit history with different score values."""
        # Arrange & Act
        history_high = AppAnnotationHitHistory(
            app_id=str(uuid4()),
            annotation_id=str(uuid4()),
            source="api",
            question="Test",
            account_id=str(uuid4()),
            score=0.99,
            message_id=str(uuid4()),
            annotation_question="Test",
            annotation_content="Content",
        )
        history_low = AppAnnotationHitHistory(
            app_id=str(uuid4()),
            annotation_id=str(uuid4()),
            source="api",
            question="Test",
            account_id=str(uuid4()),
            score=0.6,
            message_id=str(uuid4()),
            annotation_question="Test",
            annotation_content="Content",
        )

        # Assert
        assert history_high.score == 0.99
        assert history_low.score == 0.6


class TestSiteModel:
    """Test suite for Site model."""

    def test_site_creation_with_required_fields(self):
        """Test creating a site with required fields."""
        # Arrange
        app_id = str(uuid4())

        # Act
        site = Site(
            app_id=app_id,
            title="Test Site",
            default_language="en-US",
            customize_token_strategy="uuid",
        )

        # Assert
        assert site.app_id == app_id
        assert site.title == "Test Site"
        assert site.default_language == "en-US"
        assert site.customize_token_strategy == "uuid"

    def test_site_creation_with_optional_fields(self):
        """Test creating a site with optional fields."""
        # Arrange & Act
        site = Site(
            app_id=str(uuid4()),
            title="Test Site",
            default_language="en-US",
            customize_token_strategy="uuid",
            icon_type=IconType.EMOJI,
            icon="🌐",
            icon_background="#0066CC",
            description="Test site description",
            copyright="© 2024 Test",
            privacy_policy="https://example.com/privacy",
        )

        # Assert
        assert site.icon_type == IconType.EMOJI
        assert site.icon == "🌐"
        assert site.icon_background == "#0066CC"
        assert site.description == "Test site description"
        assert site.copyright == "© 2024 Test"
        assert site.privacy_policy == "https://example.com/privacy"

    def test_site_custom_disclaimer_setter(self):
        """Test site custom_disclaimer setter."""
        # Arrange
        site = Site(
            app_id=str(uuid4()),
            title="Test Site",
            default_language="en-US",
            customize_token_strategy="uuid",
        )

        # Act
        site.custom_disclaimer = "This is a test disclaimer"

        # Assert
        assert site.custom_disclaimer == "This is a test disclaimer"

    def test_site_custom_disclaimer_exceeds_limit(self):
        """Test site custom_disclaimer with excessive length."""
        # Arrange
        site = Site(
            app_id=str(uuid4()),
            title="Test Site",
            default_language="en-US",
            customize_token_strategy="uuid",
        )
        long_disclaimer = "x" * 513  # Exceeds 512 character limit

        # Act & Assert
        with pytest.raises(ValueError, match="Custom disclaimer cannot exceed 512 characters"):
            site.custom_disclaimer = long_disclaimer


class TestModelIntegration:
    """Test suite for model integration scenarios."""

    def test_complete_app_conversation_message_hierarchy(self):
        """Test complete hierarchy from app to message."""
        # Arrange
        tenant_id = str(uuid4())
        app_id = str(uuid4())
        conversation_id = str(uuid4())
        message_id = str(uuid4())
        created_by = str(uuid4())

        # Create app
        app = App(
            tenant_id=tenant_id,
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=True,
            created_by=created_by,
        )
        app.id = app_id

        # Create conversation
        conversation = Conversation(
            app_id=app_id,
            mode=AppMode.CHAT,
            name="Test Conversation",
            status="normal",
            from_source=ConversationFromSource.API,
            from_end_user_id=str(uuid4()),
        )
        conversation.id = conversation_id

        # Create message
        message = Message(
            app_id=app_id,
            conversation_id=conversation_id,
            query="Test query",
            message={"role": "user", "content": "Test"},
            answer="Test answer",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            currency="USD",
            from_source=ConversationFromSource.API,
        )
        message.id = message_id

        # Assert
        assert app.id == app_id
        assert conversation.app_id == app_id
        assert message.app_id == app_id
        assert message.conversation_id == conversation_id
        assert app.mode == AppMode.CHAT
        assert conversation.mode == AppMode.CHAT

    def test_app_with_annotation_setting(self):
        """Test app with annotation setting."""
        # Arrange
        app_id = str(uuid4())
        collection_binding_id = str(uuid4())
        created_user_id = str(uuid4())

        # Create app
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=True,
            created_by=created_user_id,
        )
        app.id = app_id

        # Create annotation setting
        setting = AppAnnotationSetting(
            app_id=app_id,
            score_threshold=0.85,
            collection_binding_id=collection_binding_id,
            created_user_id=created_user_id,
            updated_user_id=created_user_id,
        )

        # Assert
        assert setting.app_id == app.id
        assert setting.score_threshold == 0.85

    def test_message_with_annotation(self):
        """Test message with annotation."""
        # Arrange
        app_id = str(uuid4())
        conversation_id = str(uuid4())
        message_id = str(uuid4())
        account_id = str(uuid4())

        # Create message
        message = Message(
            app_id=app_id,
            conversation_id=conversation_id,
            query="What is AI?",
            message={"role": "user", "content": "What is AI?"},
            answer="AI stands for Artificial Intelligence.",
            message_unit_price=Decimal("0.0001"),
            answer_unit_price=Decimal("0.0002"),
            currency="USD",
            from_source=ConversationFromSource.API,
        )
        message.id = message_id

        # Create annotation
        annotation = MessageAnnotation(
            app_id=app_id,
            conversation_id=conversation_id,
            message_id=message_id,
            question="What is AI?",
            content="AI stands for Artificial Intelligence.",
            account_id=account_id,
        )

        # Assert
        assert annotation.app_id == message.app_id
        assert annotation.conversation_id == message.conversation_id
        assert annotation.message_id == message.id

    def test_annotation_hit_history_tracking(self):
        """Test annotation hit history tracking."""
        # Arrange
        app_id = str(uuid4())
        annotation_id = str(uuid4())
        message_id = str(uuid4())
        account_id = str(uuid4())

        # Create annotation
        annotation = MessageAnnotation(
            app_id=app_id,
            question="What is AI?",
            content="AI stands for Artificial Intelligence.",
            conversation_id=None,
            message_id=message_id,
            account_id=account_id,
        )
        annotation.id = annotation_id

        # Create hit history
        history = AppAnnotationHitHistory(
            app_id=app_id,
            annotation_id=annotation_id,
            source="api",
            question="What is AI?",
            account_id=account_id,
            score=0.92,
            message_id=message_id,
            annotation_question="What is AI?",
            annotation_content="AI stands for Artificial Intelligence.",
        )

        # Assert
        assert history.app_id == annotation.app_id
        assert history.annotation_id == annotation.id
        assert history.score == 0.92

    def test_app_with_site(self):
        """Test app with site."""
        # Arrange
        app_id = str(uuid4())

        # Create app
        app = App(
            tenant_id=str(uuid4()),
            name="Test App",
            mode=AppMode.CHAT,
            enable_site=True,
            enable_api=True,
            created_by=str(uuid4()),
        )
        app.id = app_id

        # Create site
        site = Site(
            app_id=app_id,
            title="Test Site",
            default_language="en-US",
            customize_token_strategy="uuid",
        )

        # Assert
        assert site.app_id == app.id
        assert app.enable_site is True
