from __future__ import annotations

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

import pytest
from fastapi import HTTPException

from core.auth.models import CurrentUser
from models.friendships import Friendship, FriendshipStatus
from models.inbox_messages import InboxMessage, InboxMessageStatus, InboxMessageType
from v1.friendships.repository import FriendshipRepository
from v1.friendships.schemas import (
    FriendRequestCreate,
)
from v1.friendships.service import FriendshipService
from v1.users.repository import UserRepository


def _create_mock_profile(
    user_id: UUID = UUID("00000000-0000-0000-0000-000000000001"),
    username: str = "testuser",
    avatar_url: str | None = None,
) -> MagicMock:
    """Create a mock Profile ORM object."""
    profile = MagicMock()
    profile.id = user_id
    profile.username = username
    profile.avatar_url = avatar_url
    profile.bio = None
    return profile


class FakeFriendshipRepo:
    """Fake repository for testing that conforms to FriendshipRepository protocol."""

    def __init__(
        self,
        friendships: list[Friendship] | None = None,
        inbox_messages: list[InboxMessage] | None = None,
    ) -> None:
        self._friendships = friendships or []
        self._inbox_messages = inbox_messages or []

    async def create_request(
        self, initiator_id: UUID, recipient_id: UUID
    ) -> tuple[Friendship, InboxMessage]:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.initiator_id = initiator_id
        friendship.user_low_id = min(initiator_id, recipient_id)
        friendship.user_high_id = max(initiator_id, recipient_id)
        friendship.status = FriendshipStatus.PENDING
        friendship.created_at = datetime.utcnow()
        self._friendships.append(friendship)

        inbox = MagicMock(spec=InboxMessage)
        inbox.id = uuid4()
        inbox.recipient_id = recipient_id
        inbox.sender_id = initiator_id
        inbox.status = InboxMessageStatus.PENDING
        inbox.message_type = InboxMessageType.FRIEND_REQUEST
        inbox.friendship_id = friendship.id
        inbox.content = None
        self._inbox_messages.append(inbox)

        return friendship, inbox

    async def get_friendship_between_users(
        self, user_id_1: UUID, user_id_2: UUID
    ) -> Friendship | None:
        for f in self._friendships:
            user_low = min(user_id_1, user_id_2)
            user_high = max(user_id_1, user_id_2)
            if f.user_low_id == user_low and f.user_high_id == user_high:
                return f
        return None

    async def get_pending_inbox_for_recipient(
        self, recipient_id: UUID, friendship_id: UUID
    ) -> InboxMessage | None:
        for msg in self._inbox_messages:
            if msg.recipient_id == recipient_id and msg.friendship_id == friendship_id:
                return msg
        return None

    async def get_friendship_by_id(self, friendship_id: UUID) -> Friendship | None:
        for f in self._friendships:
            if f.id == friendship_id:
                return f
        return None

    async def get_inbox_messages_for_user(
        self, user_id: UUID, status: InboxMessageStatus | None = None
    ) -> list[InboxMessage]:
        result = [msg for msg in self._inbox_messages if msg.recipient_id == user_id]
        if status:
            result = [msg for msg in result if msg.status == status]
        return result

    async def get_outgoing_requests(self, user_id: UUID) -> list[Friendship]:
        return [
            f
            for f in self._friendships
            if f.initiator_id == user_id and f.status == FriendshipStatus.PENDING
        ]

    async def get_friends_list(self, user_id: UUID) -> list[Friendship]:
        return [
            f
            for f in self._friendships
            if f.status == FriendshipStatus.ACCEPTED
            and (f.user_low_id == user_id or f.user_high_id == user_id)
        ]


class FakeUserRepo:
    """Fake user repository for testing."""

    def __init__(self, profiles: dict[UUID, MagicMock] | None = None) -> None:
        self._profiles = profiles or {}

    async def get_by_user_id(self, user_id: UUID) -> MagicMock | None:
        return self._profiles.get(user_id)

    async def get_by_username(self, username: str) -> MagicMock | None:
        for profile in self._profiles.values():
            if profile.username == username:
                return profile
        return None


_repo_check: FriendshipRepository = FakeFriendshipRepo()
_user_repo_check: UserRepository = FakeUserRepo()

USER_A = UUID("00000000-0000-0000-0000-000000000001")
USER_B = UUID("00000000-0000-0000-0000-000000000002")
USER_C = UUID("00000000-0000-0000-0000-000000000003")


@pytest.fixture
def mock_session() -> AsyncMock:
    session = AsyncMock()
    session.commit = AsyncMock()
    session.rollback = AsyncMock()
    return session


@pytest.fixture
def current_user() -> CurrentUser:
    return CurrentUser(id=USER_A)


@pytest.fixture
def mock_friendship_repo() -> FakeFriendshipRepo:
    return FakeFriendshipRepo()


@pytest.fixture
def mock_user_repo() -> FakeUserRepo:
    return FakeUserRepo(
        {
            USER_A: _create_mock_profile(USER_A, "user_a"),
            USER_B: _create_mock_profile(USER_B, "user_b"),
            USER_C: _create_mock_profile(USER_C, "user_c"),
        }
    )


class TestSendRequest:
    @pytest.mark.asyncio
    async def test_send_request_creates_friendship_and_inbox(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        result = await service.send_request(FriendRequestCreate(target_user_id=USER_B))

        assert result is not None
        mock_session.commit.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_send_request_to_self_raises_400(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        with pytest.raises(HTTPException) as exc_info:
            await service.send_request(
                FriendRequestCreate(target_user_id=current_user.id)
            )

        assert exc_info.value.status_code == 400

    @pytest.mark.asyncio
    async def test_send_request_to_existing_friend_raises_400(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        existing_friendship = MagicMock(spec=Friendship)
        existing_friendship.id = uuid4()
        existing_friendship.user_low_id = min(USER_A, USER_B)
        existing_friendship.user_high_id = max(USER_A, USER_B)
        existing_friendship.status = FriendshipStatus.ACCEPTED
        mock_friendship_repo._friendships.append(existing_friendship)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        with pytest.raises(HTTPException) as exc_info:
            await service.send_request(FriendRequestCreate(target_user_id=USER_B))

        assert exc_info.value.status_code == 400

    @pytest.mark.asyncio
    async def test_send_request_when_blocked_raises_400(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        blocked_friendship = MagicMock(spec=Friendship)
        blocked_friendship.id = uuid4()
        blocked_friendship.user_low_id = min(USER_A, USER_B)
        blocked_friendship.user_high_id = max(USER_A, USER_B)
        blocked_friendship.status = FriendshipStatus.BLOCKED
        mock_friendship_repo._friendships.append(blocked_friendship)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        with pytest.raises(HTTPException) as exc_info:
            await service.send_request(FriendRequestCreate(target_user_id=USER_B))

        assert exc_info.value.status_code == 400


class TestAcceptRequest:
    @pytest.mark.asyncio
    async def test_accept_request_updates_status(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.initiator_id = USER_B
        friendship.user_low_id = min(USER_A, USER_B)
        friendship.user_high_id = max(USER_A, USER_B)
        friendship.status = FriendshipStatus.PENDING
        mock_friendship_repo._friendships.append(friendship)

        inbox = MagicMock(spec=InboxMessage)
        inbox.id = uuid4()
        inbox.recipient_id = USER_A  # current_user is the recipient
        inbox.content = None
        inbox.status = InboxMessageStatus.PENDING
        inbox.friendship_id = friendship.id
        mock_friendship_repo._inbox_messages.append(inbox)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        result = await service.accept_request(friendship.id)

        assert result.status == "accepted"
        mock_session.commit.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_accept_nonexistent_request_raises_404(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        with pytest.raises(HTTPException) as exc_info:
            await service.accept_request(uuid4())

        assert exc_info.value.status_code == 404

    @pytest.mark.asyncio
    async def test_accept_request_not_recipient_raises_403(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.initiator_id = USER_A  # current user is the initiator
        friendship.user_low_id = min(USER_A, USER_B)
        friendship.user_high_id = max(USER_A, USER_B)
        friendship.status = FriendshipStatus.PENDING
        mock_friendship_repo._friendships.append(friendship)

        inbox = MagicMock(spec=InboxMessage)
        inbox.id = uuid4()
        inbox.recipient_id = USER_B  # the other user is the recipient
        inbox.content = None
        inbox.status = InboxMessageStatus.PENDING
        inbox.friendship_id = friendship.id
        mock_friendship_repo._inbox_messages.append(inbox)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        with pytest.raises(HTTPException) as exc_info:
            await service.accept_request(friendship.id)

        assert exc_info.value.status_code == 403


class TestDeclineRequest:
    @pytest.mark.asyncio
    async def test_decline_request_updates_status(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.initiator_id = USER_B
        friendship.user_low_id = min(USER_A, USER_B)
        friendship.user_high_id = max(USER_A, USER_B)
        friendship.status = FriendshipStatus.PENDING
        mock_friendship_repo._friendships.append(friendship)

        inbox = MagicMock(spec=InboxMessage)
        inbox.id = uuid4()
        inbox.recipient_id = USER_A
        inbox.content = None
        inbox.status = InboxMessageStatus.PENDING
        inbox.friendship_id = friendship.id
        mock_friendship_repo._inbox_messages.append(inbox)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        result = await service.decline_request(friendship.id)

        assert result.status == "rejected"
        mock_session.commit.assert_awaited_once()


class TestCancelRequest:
    @pytest.mark.asyncio
    async def test_cancel_request_removes_request(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.initiator_id = current_user.id
        friendship.user_low_id = min(USER_A, USER_B)
        friendship.user_high_id = max(USER_A, USER_B)
        friendship.status = FriendshipStatus.PENDING
        mock_friendship_repo._friendships.append(friendship)

        inbox = MagicMock(spec=InboxMessage)
        inbox.id = uuid4()
        inbox.recipient_id = USER_B  # The other user
        inbox.content = None
        inbox.status = InboxMessageStatus.PENDING
        inbox.friendship_id = friendship.id
        mock_friendship_repo._inbox_messages.append(inbox)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        result = await service.cancel_request(friendship.id)

        assert result.status == "canceled"
        mock_session.commit.assert_awaited_once()


class TestGetInbox:
    @pytest.mark.asyncio
    async def test_get_inbox_returns_pending_messages(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.initiator_id = USER_B
        friendship.status = FriendshipStatus.PENDING
        mock_friendship_repo._friendships.append(friendship)

        inbox = MagicMock(spec=InboxMessage)
        inbox.id = uuid4()
        inbox.recipient_id = current_user.id
        inbox.sender_id = USER_B
        inbox.content = None
        inbox.status = InboxMessageStatus.PENDING
        inbox.message_type = InboxMessageType.FRIEND_REQUEST
        inbox.friendship_id = friendship.id
        inbox.created_at = datetime.utcnow()
        mock_friendship_repo._inbox_messages.append(inbox)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        result = await service.get_inbox()

        assert len(result) == 1


class TestGetOutgoingRequests:
    @pytest.mark.asyncio
    async def test_get_outgoing_requests_returns_sent_requests(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.initiator_id = current_user.id
        friendship.status = FriendshipStatus.PENDING
        mock_friendship_repo._friendships.append(friendship)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        result = await service.get_outgoing_requests()

        assert len(result) == 1


class TestGetFriendsList:
    @pytest.mark.asyncio
    async def test_get_friends_list_returns_accepted_friends(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.user_low_id = current_user.id
        friendship.user_high_id = USER_B
        friendship.status = FriendshipStatus.ACCEPTED
        friendship.created_at = datetime.utcnow()
        friendship.updated_at = datetime.utcnow()
        mock_friendship_repo._friendships.append(friendship)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        result = await service.get_friends_list()

        assert len(result) == 1


class TestRemoveFriend:
    @pytest.mark.asyncio
    async def test_remove_friend_soft_deletes(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        friendship = MagicMock(spec=Friendship)
        friendship.id = uuid4()
        friendship.user_low_id = min(current_user.id, USER_B)
        friendship.user_high_id = max(current_user.id, USER_B)
        friendship.status = FriendshipStatus.ACCEPTED
        friendship.created_at = datetime.utcnow()
        friendship.updated_at = datetime.utcnow()
        mock_friendship_repo._friendships.append(friendship)

        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        result = await service.remove_friend(USER_B)

        assert result.status == "active"
        mock_session.commit.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_remove_nonexistent_friend_raises_404(
        self,
        mock_session: AsyncMock,
        mock_friendship_repo: FakeFriendshipRepo,
        mock_user_repo: FakeUserRepo,
        current_user: CurrentUser,
    ) -> None:
        service = FriendshipService(
            repository=mock_friendship_repo,
            user_repository=mock_user_repo,
            session=mock_session,
            current_user=current_user,
        )

        with pytest.raises(HTTPException) as exc_info:
            await service.remove_friend(uuid4())

        assert exc_info.value.status_code == 404
