import uuid
from datetime import datetime, UTC

import pytest
from unittest.mock import patch, MagicMock, AsyncMock
from fastapi import HTTPException

from preloop.api.endpoints import comments
from preloop.schemas.comment import CommentCreate


@pytest.fixture
def mock_user():
    """Create a mock user."""
    user = MagicMock()
    user.id = uuid.uuid4()
    user.account_id = str(uuid.uuid4())
    return user


@pytest.fixture
def mock_tracker():
    """Create a mock tracker."""
    tracker = MagicMock()
    tracker.id = str(uuid.uuid4())
    return tracker


@pytest.fixture
def mock_issue(mock_tracker):
    """Create a mock issue."""
    issue = MagicMock()
    issue.id = str(uuid.uuid4())
    issue.project_id = str(uuid.uuid4())
    issue.tracker_id = mock_tracker.id
    return issue


@pytest.fixture
def mock_project():
    """Create a mock project."""
    project = MagicMock()
    project.id = str(uuid.uuid4())
    project.organization_id = str(uuid.uuid4())
    return project


@pytest.fixture
def mock_comment(mock_issue):
    """Create a mock comment."""
    comment = MagicMock()
    comment.id = str(uuid.uuid4())
    comment.issue_id = mock_issue.id
    comment.body = "Test comment"
    comment.author = "testuser"
    comment.created_at = datetime.now(UTC)
    comment.updated_at = datetime.now(UTC)
    return comment


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.get_tracker_client")
async def test_list_issue_comments_success(mock_get_tracker_client):
    """
    Tests the list_issue_comments function for a successful request.
    """
    tracker_client = AsyncMock()
    tracker_client.get_issue.return_value = MagicMock()
    tracker_client.get_comments.return_value = []
    mock_get_tracker_client.return_value = tracker_client

    db_session = MagicMock()
    current_user = MagicMock()

    result = await comments.list_issue_comments(
        "123",
        "org",
        "proj",
        limit=10,
        offset=0,
        db=db_session,
        current_user=current_user,
    )
    assert result.total == 0


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.get_tracker_client")
async def test_list_issue_comments_with_data(mock_get_tracker_client):
    """Test listing comments with actual comment data."""
    mock_comment = MagicMock()
    mock_comment.id = "comment-1"
    mock_comment.author = "testuser"
    mock_comment.body = "Test comment body"
    mock_comment.created_at = datetime.now(UTC)
    mock_comment.updated_at = datetime.now(UTC)
    mock_comment.metadata = {}

    tracker_client = AsyncMock()
    tracker_client.get_issue.return_value = MagicMock()
    tracker_client.get_comments.return_value = [mock_comment]
    mock_get_tracker_client.return_value = tracker_client

    db_session = MagicMock()
    current_user = MagicMock()

    result = await comments.list_issue_comments(
        "123",
        "org",
        "proj",
        limit=10,
        offset=0,
        db=db_session,
        current_user=current_user,
    )
    assert result.total == 1
    assert len(result.items) == 1
    assert result.items[0].body == "Test comment body"


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.get_tracker_client")
async def test_list_issue_comments_issue_not_found(mock_get_tracker_client):
    """Test 404 when issue is not found."""
    tracker_client = AsyncMock()
    tracker_client.get_issue.return_value = None
    mock_get_tracker_client.return_value = tracker_client

    db_session = MagicMock()
    current_user = MagicMock()

    with pytest.raises(HTTPException) as exc_info:
        await comments.list_issue_comments(
            "non-existent",
            "org",
            "proj",
            limit=10,
            offset=0,
            db=db_session,
            current_user=current_user,
        )

    assert exc_info.value.status_code == 404
    assert exc_info.value.detail == "Issue not found"


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.get_tracker_client")
async def test_list_issue_comments_pagination(mock_get_tracker_client):
    """Test pagination in list_issue_comments."""
    mock_comments = [
        MagicMock(
            id=f"comment-{i}",
            author="testuser",
            body=f"Comment {i}",
            created_at=datetime.now(UTC),
            updated_at=datetime.now(UTC),
            metadata={},
        )
        for i in range(5)
    ]

    tracker_client = AsyncMock()
    tracker_client.get_issue.return_value = MagicMock()
    tracker_client.get_comments.return_value = mock_comments
    mock_get_tracker_client.return_value = tracker_client

    db_session = MagicMock()
    current_user = MagicMock()

    # Test with offset
    result = await comments.list_issue_comments(
        "123",
        "org",
        "proj",
        limit=2,
        offset=2,
        db=db_session,
        current_user=current_user,
    )
    assert result.total == 5
    assert len(result.items) == 2


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.get_tracker_client")
async def test_list_issue_comments_error(mock_get_tracker_client):
    """Test error handling in list_issue_comments."""
    tracker_client = AsyncMock()
    tracker_client.get_issue.side_effect = Exception("Connection error")
    mock_get_tracker_client.return_value = tracker_client

    db_session = MagicMock()
    current_user = MagicMock()

    with pytest.raises(HTTPException) as exc_info:
        await comments.list_issue_comments(
            "123",
            "org",
            "proj",
            limit=10,
            offset=0,
            db=db_session,
            current_user=current_user,
        )

    assert exc_info.value.status_code == 500


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.get_tracker_client")
async def test_add_issue_comment_success(mock_get_tracker_client):
    """
    Tests the add_issue_comment function for a successful request.
    """
    tracker_client = AsyncMock()
    tracker_client.get_issue.return_value = MagicMock()
    tracker_client.add_comment.return_value = MagicMock(
        id="456",
        author="test",
        body="Test comment",
        created_at="2025-09-22T00:19:39.601Z",
        updated_at="2025-09-22T00:19:39.601Z",
        meta_data={},
    )
    mock_get_tracker_client.return_value = tracker_client

    db_session = MagicMock()
    current_user = MagicMock()
    comment_create = CommentCreate(body="Test comment")

    result = await comments.add_issue_comment(
        "123", comment_create, "org", "proj", db=db_session, current_user=current_user
    )
    assert result.id == "456"


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.get_tracker_client")
async def test_add_issue_comment_issue_not_found(mock_get_tracker_client):
    """Test 404 when issue is not found."""
    tracker_client = AsyncMock()
    tracker_client.get_issue.return_value = None
    mock_get_tracker_client.return_value = tracker_client

    db_session = MagicMock()
    current_user = MagicMock()
    comment_create = CommentCreate(body="Test comment")

    with pytest.raises(HTTPException) as exc_info:
        await comments.add_issue_comment(
            "non-existent",
            comment_create,
            "org",
            "proj",
            db=db_session,
            current_user=current_user,
        )

    assert exc_info.value.status_code == 404


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.get_tracker_client")
async def test_add_issue_comment_error(mock_get_tracker_client):
    """Test error handling in add_issue_comment."""
    tracker_client = AsyncMock()
    tracker_client.get_issue.return_value = MagicMock()
    tracker_client.add_comment.side_effect = Exception("Failed to add comment")
    mock_get_tracker_client.return_value = tracker_client

    db_session = MagicMock()
    current_user = MagicMock()
    comment_create = CommentCreate(body="Test comment")

    with pytest.raises(HTTPException) as exc_info:
        await comments.add_issue_comment(
            "123",
            comment_create,
            "org",
            "proj",
            db=db_session,
            current_user=current_user,
        )

    assert exc_info.value.status_code == 500


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.crud_comment.get_multi_by_issue")
async def test_search_comments_by_issue_id(mock_get_multi_by_issue):
    """
    Tests the search_comments function when searching by issue_id.
    """
    mock_get_multi_by_issue.return_value = []
    db_session = MagicMock()
    current_user = MagicMock()

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[MagicMock()],
    ):
        result = await comments.search_comments(
            db=db_session,
            current_user=current_user,
            issue_id="123",
            search_type="fulltext",
            query="",
            author=None,
        )
        assert result.total == 0
        mock_get_multi_by_issue.assert_called_once()


@pytest.mark.asyncio
@patch("preloop.api.endpoints.comments.crud_comment.get_multi_by_author")
async def test_search_comments_by_author(mock_get_multi_by_author):
    """
    Tests the search_comments function when searching by author.
    """
    mock_get_multi_by_author.return_value = []
    db_session = MagicMock()
    current_user = MagicMock()

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[MagicMock()],
    ):
        result = await comments.search_comments(
            db=db_session,
            current_user=current_user,
            author="test",
            search_type="fulltext",
            query="",
            issue_id=None,
        )
        assert result.total == 0
        mock_get_multi_by_author.assert_called_once()


@pytest.mark.asyncio
async def test_search_comments_no_trackers(mock_user):
    """Test search returns empty when user has no trackers."""
    db_session = MagicMock()

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[],
    ):
        result = await comments.search_comments(
            db=db_session,
            current_user=mock_user,
            query="test",
            search_type="fulltext",
        )

        assert result.total == 0
        assert result.items == []


@pytest.mark.asyncio
async def test_search_comments_similarity_no_model(mock_user, mock_tracker):
    """Test similarity search fails when no embedding model is configured."""
    db_session = MagicMock()

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[mock_tracker],
    ):
        with patch(
            "preloop.api.endpoints.comments.crud_embedding_model.get_active",
            return_value=[],
        ):
            with pytest.raises(HTTPException) as exc_info:
                await comments.search_comments(
                    db=db_session,
                    current_user=mock_user,
                    query="test",
                    search_type="similarity",
                )

            assert exc_info.value.status_code == 500
            assert "No active embedding model" in exc_info.value.detail


@pytest.mark.asyncio
async def test_search_comments_similarity_success(
    mock_user, mock_tracker, mock_issue, mock_project, mock_comment
):
    """Test successful similarity search."""
    db_session = MagicMock()
    db_session.get.side_effect = lambda model, id: {
        mock_issue.id: mock_issue,
        mock_project.id: mock_project,
    }.get(id)

    mock_model = MagicMock()
    mock_model.id = str(uuid.uuid4())

    # Update mock_issue to match tracker
    mock_issue.tracker_id = mock_tracker.id
    mock_issue.project_id = mock_project.id

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[mock_tracker],
    ):
        with patch(
            "preloop.api.endpoints.comments.crud_embedding_model.get_active",
            return_value=[mock_model],
        ):
            with patch(
                "preloop.api.endpoints.comments.crud_issue_embedding._generate_embedding_vector",
                return_value=[0.1] * 1536,
            ):
                with patch(
                    "preloop.api.endpoints.comments.crud_issue_embedding.similarity_search",
                    return_value=[(mock_comment, 0.95)],
                ):
                    result = await comments.search_comments(
                        db=db_session,
                        current_user=mock_user,
                        query="test query",
                        search_type="similarity",
                    )

                    assert result.total == 1


@pytest.mark.asyncio
async def test_search_comments_invalid_search_type(mock_user, mock_tracker):
    """Test error for invalid search type."""
    db_session = MagicMock()

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[mock_tracker],
    ):
        with pytest.raises(HTTPException) as exc_info:
            await comments.search_comments(
                db=db_session,
                current_user=mock_user,
                query="test",
                search_type="invalid",
            )

        assert exc_info.value.status_code == 400
        assert "Invalid search_type" in exc_info.value.detail


@pytest.mark.asyncio
async def test_search_comments_fulltext_no_filters(mock_user, mock_tracker):
    """Test fulltext search returns empty when no filters are provided."""
    db_session = MagicMock()

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[mock_tracker],
    ):
        result = await comments.search_comments(
            db=db_session,
            current_user=mock_user,
            query="test",
            search_type="fulltext",
            author=None,
            issue_id=None,
        )

        assert result.total == 0
        assert result.items == []


@pytest.mark.asyncio
async def test_search_comments_similarity_vector_error(mock_user, mock_tracker):
    """Test error handling when vector generation fails."""
    db_session = MagicMock()

    mock_model = MagicMock()
    mock_model.id = str(uuid.uuid4())

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[mock_tracker],
    ):
        with patch(
            "preloop.api.endpoints.comments.crud_embedding_model.get_active",
            return_value=[mock_model],
        ):
            with patch(
                "preloop.api.endpoints.comments.crud_issue_embedding._generate_embedding_vector",
                side_effect=Exception("Vector generation failed"),
            ):
                with pytest.raises(HTTPException) as exc_info:
                    await comments.search_comments(
                        db=db_session,
                        current_user=mock_user,
                        query="test query",
                        search_type="similarity",
                    )

                assert exc_info.value.status_code == 500
                assert "Error generating query vector" in exc_info.value.detail


@pytest.mark.asyncio
async def test_search_comments_fulltext_with_results(
    mock_user, mock_tracker, mock_issue, mock_project, mock_comment
):
    """Test fulltext search with actual results."""
    db_session = MagicMock()
    db_session.get.side_effect = lambda model, id: {
        mock_issue.id: mock_issue,
        mock_project.id: mock_project,
    }.get(id)

    mock_issue.tracker_id = mock_tracker.id
    mock_issue.project_id = mock_project.id

    with patch(
        "preloop.api.endpoints.comments.crud_tracker.get_for_account",
        return_value=[mock_tracker],
    ):
        with patch(
            "preloop.api.endpoints.comments.crud_comment.get_multi_by_author",
            return_value=[mock_comment],
        ):
            result = await comments.search_comments(
                db=db_session,
                current_user=mock_user,
                author="testuser",
                search_type="fulltext",
                query="",  # Must pass a string, not use default Query() object
            )

            assert result.total == 1
            assert len(result.items) == 1
