"""Unit tests for the V1 skills endpoint in app_conversation_router.

This module tests the GET /{conversation_id}/skills endpoint functionality,
following TDD best practices with AAA structure.
"""

from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4

import pytest
from fastapi import status

from openhands.app_server.app_conversation.app_conversation_models import (
    AppConversation,
)
from openhands.app_server.app_conversation.app_conversation_router import (
    get_conversation_skills,
)
from openhands.app_server.app_conversation.app_conversation_service_base import (
    AppConversationServiceBase,
)
from openhands.app_server.sandbox.sandbox_models import (
    AGENT_SERVER,
    ExposedUrl,
    SandboxInfo,
    SandboxStatus,
)
from openhands.app_server.sandbox.sandbox_spec_models import SandboxSpecInfo
from openhands.app_server.user.user_context import UserContext
from openhands.sdk.skills import KeywordTrigger, Skill, TaskTrigger


def _make_service_mock(
    *,
    user_context: UserContext,
    conversation_return: AppConversation | None = None,
    skills_return: list[Skill] | None = None,
    raise_on_load: bool = False,
):
    """Create a mock service that passes the isinstance check and returns the desired values."""
    mock_cls = type('AppConversationServiceMock', (MagicMock,), {})
    AppConversationServiceBase.register(mock_cls)

    service = mock_cls()
    service.user_context = user_context
    service.get_app_conversation = AsyncMock(return_value=conversation_return)

    async def _load_skills(*_args, **_kwargs):
        if raise_on_load:
            raise Exception('Skill loading failed')
        return skills_return or []

    service.load_and_merge_all_skills = AsyncMock(side_effect=_load_skills)
    return service


@pytest.mark.asyncio
class TestGetConversationSkills:
    """Test suite for get_conversation_skills endpoint."""

    async def test_get_skills_returns_repo_and_knowledge_skills(self):
        """Test successful retrieval of both repo and knowledge skills.

        Arrange: Setup conversation, sandbox, and skills with different types
        Act: Call get_conversation_skills endpoint
        Assert: Response contains both repo and knowledge skills with correct types
        """
        # Arrange
        conversation_id = uuid4()
        sandbox_id = str(uuid4())
        working_dir = '/workspace'

        # Create mock conversation
        mock_conversation = AppConversation(
            id=conversation_id,
            created_by_user_id='test-user',
            sandbox_id=sandbox_id,
            selected_repository='owner/repo',
            sandbox_status=SandboxStatus.RUNNING,
        )

        # Create mock sandbox with agent server URL
        mock_sandbox = SandboxInfo(
            id=sandbox_id,
            created_by_user_id='test-user',
            status=SandboxStatus.RUNNING,
            sandbox_spec_id=str(uuid4()),
            session_api_key='test-api-key',
            exposed_urls=[
                ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
            ],
        )

        # Create mock sandbox spec
        mock_sandbox_spec = SandboxSpecInfo(
            id=str(uuid4()), command=None, working_dir=working_dir
        )

        # Create mock skills - repo skill (no trigger)
        repo_skill = Skill(
            name='repo_skill',
            content='Repository skill content',
            trigger=None,
        )

        # Create mock skills - knowledge skill (with KeywordTrigger)
        knowledge_skill = Skill(
            name='knowledge_skill',
            content='Knowledge skill content',
            trigger=KeywordTrigger(keywords=['test', 'help']),
        )

        # Mock services
        mock_user_context = MagicMock(spec=UserContext)
        mock_app_conversation_service = _make_service_mock(
            user_context=mock_user_context,
            conversation_return=mock_conversation,
            skills_return=[repo_skill, knowledge_skill],
        )

        mock_sandbox_service = MagicMock()
        mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)

        mock_sandbox_spec_service = MagicMock()
        mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
            return_value=mock_sandbox_spec
        )

        # Act
        response = await get_conversation_skills(
            conversation_id=conversation_id,
            app_conversation_service=mock_app_conversation_service,
            sandbox_service=mock_sandbox_service,
            sandbox_spec_service=mock_sandbox_spec_service,
        )

        # Assert
        assert response.status_code == status.HTTP_200_OK
        content = response.body.decode('utf-8')
        import json

        data = json.loads(content)
        assert 'skills' in data
        assert len(data['skills']) == 2

        # Check repo skill
        repo_skill_data = next(
            (s for s in data['skills'] if s['name'] == 'repo_skill'), None
        )
        assert repo_skill_data is not None
        assert repo_skill_data['type'] == 'repo'
        assert repo_skill_data['content'] == 'Repository skill content'
        assert repo_skill_data['triggers'] == []

        # Check knowledge skill
        knowledge_skill_data = next(
            (s for s in data['skills'] if s['name'] == 'knowledge_skill'), None
        )
        assert knowledge_skill_data is not None
        assert knowledge_skill_data['type'] == 'knowledge'
        assert knowledge_skill_data['content'] == 'Knowledge skill content'
        assert knowledge_skill_data['triggers'] == ['test', 'help']

    async def test_get_skills_returns_404_when_conversation_not_found(self):
        """Test endpoint returns 404 when conversation doesn't exist.

        Arrange: Setup mocks to return None for conversation
        Act: Call get_conversation_skills endpoint
        Assert: Response is 404 with appropriate error message
        """
        # Arrange
        conversation_id = uuid4()

        mock_user_context = MagicMock(spec=UserContext)
        mock_app_conversation_service = _make_service_mock(
            user_context=mock_user_context,
            conversation_return=None,
        )

        mock_sandbox_service = MagicMock()
        mock_sandbox_spec_service = MagicMock()

        # Act
        response = await get_conversation_skills(
            conversation_id=conversation_id,
            app_conversation_service=mock_app_conversation_service,
            sandbox_service=mock_sandbox_service,
            sandbox_spec_service=mock_sandbox_spec_service,
        )

        # Assert
        assert response.status_code == status.HTTP_404_NOT_FOUND
        content = response.body.decode('utf-8')
        import json

        data = json.loads(content)
        assert 'error' in data
        assert str(conversation_id) in data['error']

    async def test_get_skills_returns_404_when_sandbox_not_found(self):
        """Test endpoint returns 404 when sandbox doesn't exist.

        Arrange: Setup conversation but no sandbox
        Act: Call get_conversation_skills endpoint
        Assert: Response is 404
        """
        # Arrange
        conversation_id = uuid4()
        sandbox_id = str(uuid4())

        mock_conversation = AppConversation(
            id=conversation_id,
            created_by_user_id='test-user',
            sandbox_id=sandbox_id,
            sandbox_status=SandboxStatus.RUNNING,
        )

        mock_user_context = MagicMock(spec=UserContext)
        mock_app_conversation_service = _make_service_mock(
            user_context=mock_user_context,
            conversation_return=mock_conversation,
        )

        mock_sandbox_service = MagicMock()
        mock_sandbox_service.get_sandbox = AsyncMock(return_value=None)

        mock_sandbox_spec_service = MagicMock()

        # Act
        response = await get_conversation_skills(
            conversation_id=conversation_id,
            app_conversation_service=mock_app_conversation_service,
            sandbox_service=mock_sandbox_service,
            sandbox_spec_service=mock_sandbox_spec_service,
        )

        # Assert
        assert response.status_code == status.HTTP_404_NOT_FOUND

    async def test_get_skills_returns_empty_list_when_sandbox_paused(self):
        """Test endpoint returns empty skills when sandbox is PAUSED (closed conversation).

        Arrange: Setup conversation with paused sandbox
        Act: Call get_conversation_skills endpoint
        Assert: Response is 200 with empty skills list
        """
        # Arrange
        conversation_id = uuid4()
        sandbox_id = str(uuid4())

        mock_conversation = AppConversation(
            id=conversation_id,
            created_by_user_id='test-user',
            sandbox_id=sandbox_id,
            sandbox_status=SandboxStatus.PAUSED,
        )

        mock_sandbox = SandboxInfo(
            id=sandbox_id,
            created_by_user_id='test-user',
            status=SandboxStatus.PAUSED,
            sandbox_spec_id=str(uuid4()),
            session_api_key='test-api-key',
        )

        mock_user_context = MagicMock(spec=UserContext)
        mock_app_conversation_service = _make_service_mock(
            user_context=mock_user_context,
            conversation_return=mock_conversation,
        )

        mock_sandbox_service = MagicMock()
        mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)

        mock_sandbox_spec_service = MagicMock()

        # Act
        response = await get_conversation_skills(
            conversation_id=conversation_id,
            app_conversation_service=mock_app_conversation_service,
            sandbox_service=mock_sandbox_service,
            sandbox_spec_service=mock_sandbox_spec_service,
        )

        # Assert
        assert response.status_code == status.HTTP_200_OK
        content = response.body.decode('utf-8')
        import json

        data = json.loads(content)
        assert data == {'skills': []}

    async def test_get_skills_handles_task_trigger_skills(self):
        """Test endpoint correctly handles skills with TaskTrigger.

        Arrange: Setup skill with TaskTrigger
        Act: Call get_conversation_skills endpoint
        Assert: Skill is categorized as knowledge type with correct triggers
        """
        # Arrange
        conversation_id = uuid4()
        sandbox_id = str(uuid4())

        mock_conversation = AppConversation(
            id=conversation_id,
            created_by_user_id='test-user',
            sandbox_id=sandbox_id,
            sandbox_status=SandboxStatus.RUNNING,
        )

        mock_sandbox = SandboxInfo(
            id=sandbox_id,
            created_by_user_id='test-user',
            status=SandboxStatus.RUNNING,
            sandbox_spec_id=str(uuid4()),
            session_api_key='test-api-key',
            exposed_urls=[
                ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
            ],
        )

        mock_sandbox_spec = SandboxSpecInfo(
            id=str(uuid4()), command=None, working_dir='/workspace'
        )

        # Create task skill with TaskTrigger
        task_skill = Skill(
            name='task_skill',
            content='Task skill content',
            trigger=TaskTrigger(triggers=['task', 'execute']),
        )

        mock_user_context = MagicMock(spec=UserContext)
        mock_app_conversation_service = _make_service_mock(
            user_context=mock_user_context,
            conversation_return=mock_conversation,
            skills_return=[task_skill],
        )

        mock_sandbox_service = MagicMock()
        mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)

        mock_sandbox_spec_service = MagicMock()
        mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
            return_value=mock_sandbox_spec
        )

        # Act
        response = await get_conversation_skills(
            conversation_id=conversation_id,
            app_conversation_service=mock_app_conversation_service,
            sandbox_service=mock_sandbox_service,
            sandbox_spec_service=mock_sandbox_spec_service,
        )

        # Assert
        assert response.status_code == status.HTTP_200_OK
        content = response.body.decode('utf-8')
        import json

        data = json.loads(content)
        assert len(data['skills']) == 1
        skill_data = data['skills'][0]
        assert skill_data['type'] == 'knowledge'
        assert skill_data['triggers'] == ['task', 'execute']

    async def test_get_skills_returns_500_on_skill_loading_error(self):
        """Test endpoint returns 500 when skill loading fails.

        Arrange: Setup mocks to raise exception during skill loading
        Act: Call get_conversation_skills endpoint
        Assert: Response is 500 with error message
        """
        # Arrange
        conversation_id = uuid4()
        sandbox_id = str(uuid4())

        mock_conversation = AppConversation(
            id=conversation_id,
            created_by_user_id='test-user',
            sandbox_id=sandbox_id,
            sandbox_status=SandboxStatus.RUNNING,
        )

        mock_sandbox = SandboxInfo(
            id=sandbox_id,
            created_by_user_id='test-user',
            status=SandboxStatus.RUNNING,
            sandbox_spec_id=str(uuid4()),
            session_api_key='test-api-key',
            exposed_urls=[
                ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
            ],
        )

        mock_sandbox_spec = SandboxSpecInfo(
            id=str(uuid4()), command=None, working_dir='/workspace'
        )

        mock_user_context = MagicMock(spec=UserContext)
        mock_app_conversation_service = _make_service_mock(
            user_context=mock_user_context,
            conversation_return=mock_conversation,
            raise_on_load=True,
        )

        mock_sandbox_service = MagicMock()
        mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)

        mock_sandbox_spec_service = MagicMock()
        mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
            return_value=mock_sandbox_spec
        )

        # Act
        response = await get_conversation_skills(
            conversation_id=conversation_id,
            app_conversation_service=mock_app_conversation_service,
            sandbox_service=mock_sandbox_service,
            sandbox_spec_service=mock_sandbox_spec_service,
        )

        # Assert
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        content = response.body.decode('utf-8')
        import json

        data = json.loads(content)
        assert 'error' in data
        assert 'Error getting skills' in data['error']

    async def test_get_skills_returns_empty_list_when_no_skills_loaded(self):
        """Test endpoint returns empty skills list when no skills are found.

        Arrange: Setup all skill loaders to return empty lists
        Act: Call get_conversation_skills endpoint
        Assert: Response contains empty skills array
        """
        # Arrange
        conversation_id = uuid4()
        sandbox_id = str(uuid4())

        mock_conversation = AppConversation(
            id=conversation_id,
            created_by_user_id='test-user',
            sandbox_id=sandbox_id,
            sandbox_status=SandboxStatus.RUNNING,
        )

        mock_sandbox = SandboxInfo(
            id=sandbox_id,
            created_by_user_id='test-user',
            status=SandboxStatus.RUNNING,
            sandbox_spec_id=str(uuid4()),
            session_api_key='test-api-key',
            exposed_urls=[
                ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
            ],
        )

        mock_sandbox_spec = SandboxSpecInfo(
            id=str(uuid4()), command=None, working_dir='/workspace'
        )

        mock_user_context = MagicMock(spec=UserContext)
        mock_app_conversation_service = _make_service_mock(
            user_context=mock_user_context,
            conversation_return=mock_conversation,
            skills_return=[],
        )

        mock_sandbox_service = MagicMock()
        mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)

        mock_sandbox_spec_service = MagicMock()
        mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
            return_value=mock_sandbox_spec
        )

        # Act
        response = await get_conversation_skills(
            conversation_id=conversation_id,
            app_conversation_service=mock_app_conversation_service,
            sandbox_service=mock_sandbox_service,
            sandbox_spec_service=mock_sandbox_spec_service,
        )

        # Assert
        assert response.status_code == status.HTTP_200_OK
        content = response.body.decode('utf-8')
        import json

        data = json.loads(content)
        assert 'skills' in data
        assert len(data['skills']) == 0

    async def test_get_skills_uses_default_sandbox_spec_when_not_found(self):
        """Test endpoint uses default sandbox spec when original spec is not found.

        Arrange: Setup sandbox with spec that returns None, but default spec available
        Act: Call get_conversation_skills endpoint
        Assert: Response is successful using default sandbox spec
        """
        # Arrange
        conversation_id = uuid4()
        sandbox_id = str(uuid4())

        mock_conversation = AppConversation(
            id=conversation_id,
            created_by_user_id='test-user',
            sandbox_id=sandbox_id,
            sandbox_status=SandboxStatus.RUNNING,
        )

        mock_sandbox = SandboxInfo(
            id=sandbox_id,
            created_by_user_id='test-user',
            status=SandboxStatus.RUNNING,
            sandbox_spec_id=str(uuid4()),
            session_api_key='test-api-key',
            exposed_urls=[
                ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
            ],
        )

        # Default sandbox spec to be used as fallback
        default_sandbox_spec = SandboxSpecInfo(
            id=str(uuid4()), command=None, working_dir='/workspace'
        )

        mock_user_context = MagicMock(spec=UserContext)
        mock_app_conversation_service = _make_service_mock(
            user_context=mock_user_context,
            conversation_return=mock_conversation,
            skills_return=[],
        )

        mock_sandbox_service = MagicMock()
        mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)

        mock_sandbox_spec_service = MagicMock()
        # Original sandbox spec lookup returns None
        mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(return_value=None)
        # Default sandbox spec is available
        mock_sandbox_spec_service.get_default_sandbox_spec = AsyncMock(
            return_value=default_sandbox_spec
        )

        # Act
        response = await get_conversation_skills(
            conversation_id=conversation_id,
            app_conversation_service=mock_app_conversation_service,
            sandbox_service=mock_sandbox_service,
            sandbox_spec_service=mock_sandbox_spec_service,
        )

        # Assert
        assert response.status_code == status.HTTP_200_OK
        # Verify that get_default_sandbox_spec was called as fallback
        mock_sandbox_spec_service.get_default_sandbox_spec.assert_called_once()
        content = response.body.decode('utf-8')
        import json

        data = json.loads(content)
        assert 'skills' in data
