# -*- coding: utf-8 -*-
"""Location: ./tests/unit/mcpgateway/services/test_team_invitation_service.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Comprehensive tests for Team Invitation Service functionality.
"""

# Standard
from unittest.mock import MagicMock, patch

# Third-Party
import pytest
from sqlalchemy.orm import Session

# First-Party
from mcpgateway.db import EmailTeam, EmailTeamInvitation, EmailTeamMember, EmailUser
from mcpgateway.services.team_invitation_service import TeamInvitationService
from mcpgateway.services.team_management_service import TeamMemberLimitExceededError


class TestTeamInvitationService:
    """Comprehensive test suite for Team Invitation Service."""

    @pytest.fixture
    def mock_db(self):
        """Create mock database session."""
        return MagicMock(spec=Session)

    @pytest.fixture
    def service(self, mock_db):
        """Create team invitation service instance."""
        svc = TeamInvitationService(mock_db)
        # Default: user is below max teams limit (0 teams)
        svc._get_user_team_count = MagicMock(return_value=0)
        return svc

    @pytest.fixture
    def mock_team(self):
        """Create mock team."""
        team = MagicMock(spec=EmailTeam)
        team.id = "team123"
        team.name = "Test Team"
        team.is_personal = False
        team.is_active = True
        team.max_members = 100
        return team

    @pytest.fixture
    def mock_user(self):
        """Create mock user."""
        user = MagicMock(spec=EmailUser)
        user.email = "user@example.com"
        user.is_active = True
        return user

    @pytest.fixture
    def mock_inviter(self):
        """Create mock inviter user."""
        user = MagicMock(spec=EmailUser)
        user.email = "admin@example.com"
        user.is_active = True
        return user

    @pytest.fixture
    def mock_membership(self):
        """Create mock team membership for inviter."""
        membership = MagicMock(spec=EmailTeamMember)
        membership.team_id = "team123"
        membership.user_email = "admin@example.com"
        membership.role = "owner"
        membership.is_active = True
        return membership

    @pytest.fixture
    def mock_invitation(self):
        """Create mock invitation."""
        invitation = MagicMock(spec=EmailTeamInvitation)
        invitation.id = "invite123"
        invitation.team_id = "team123"
        invitation.email = "user@example.com"
        invitation.role = "member"
        invitation.invited_by = "admin@example.com"
        invitation.token = "secure_token_123"
        invitation.is_active = True
        invitation.is_valid.return_value = True
        invitation.is_expired.return_value = False
        return invitation

    # =========================================================================
    # Service Initialization Tests
    # =========================================================================

    def test_service_initialization(self, mock_db):
        """Test service initialization."""
        service = TeamInvitationService(mock_db)

        assert service.db == mock_db
        assert service.db is not None

    def test_service_has_required_methods(self, service):
        """Test that service has all required methods."""
        required_methods = [
            "create_invitation",
            "get_invitation_by_token",
            "accept_invitation",
            "decline_invitation",
            "revoke_invitation",
            "get_team_invitations",
            "get_user_invitations",
            "cleanup_expired_invitations",
        ]

        for method_name in required_methods:
            assert hasattr(service, method_name)
            assert callable(getattr(service, method_name))

    def test_generate_invitation_token(self, service):
        """Test invitation token generation."""
        token1 = service._generate_invitation_token()
        token2 = service._generate_invitation_token()

        # Tokens should be strings
        assert isinstance(token1, str)
        assert isinstance(token2, str)

        # Tokens should be different
        assert token1 != token2

        # Tokens should be of reasonable length (32 bytes base64 encoded)
        assert len(token1) >= 40  # urlsafe_b64encode adds padding

    # =========================================================================
    # Invitation Creation Tests
    # =========================================================================

    @pytest.mark.skip("Complex integration test - main functionality covered by simpler tests")
    @pytest.mark.asyncio
    async def test_create_invitation_success(self, service, mock_db):
        """Test successful invitation creation."""
        # Create fresh mocks with proper attributes
        mock_team = MagicMock(spec=EmailTeam)
        mock_team.id = "team123"
        mock_team.is_personal = False
        mock_team.max_members = 100

        mock_inviter = MagicMock(spec=EmailUser)
        mock_inviter.email = "admin@example.com"

        mock_membership = MagicMock(spec=EmailTeamMember)
        mock_membership.role = "owner"

        # Simple query side effect that returns appropriate values
        call_counts = {"team": 0, "user": 0, "member": 0, "invitation": 0}

        def simple_query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                call_counts["team"] += 1
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                call_counts["user"] += 1
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                call_counts["member"] += 1
                if call_counts["member"] == 1:
                    # Inviter membership check
                    mock_query.filter.return_value.first.return_value = mock_membership
                elif call_counts["member"] == 2:
                    # Check if invitee is already a member
                    mock_query.filter.return_value.first.return_value = None
                else:
                    # Member count check
                    mock_query.filter.return_value.count.return_value = 5
            elif model == EmailTeamInvitation:
                call_counts["invitation"] += 1
                if call_counts["invitation"] == 1:
                    # Check existing invitations
                    mock_query.filter.return_value.first.return_value = None
                else:
                    # Pending invitation count
                    mock_query.filter.return_value.count.return_value = 2

            return mock_query

        mock_db.query.side_effect = simple_query_side_effect

        with (
            patch("mcpgateway.services.team_invitation_service.EmailTeamInvitation") as MockInvitation,
            patch("mcpgateway.services.team_invitation_service.utc_now"),
            patch("mcpgateway.services.team_invitation_service.timedelta"),
        ):
            mock_invitation_instance = MagicMock()
            MockInvitation.return_value = mock_invitation_instance

            result = await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

            assert result == mock_invitation_instance
            mock_db.add.assert_called_once_with(mock_invitation_instance)
            mock_db.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_create_invitation_invalid_role(self, service):
        """Test creating invitation with invalid role."""
        with pytest.raises(ValueError, match="Invalid role"):
            await service.create_invitation(team_id="team123", email="user@example.com", role="invalid", invited_by="admin@example.com")

    @pytest.mark.asyncio
    async def test_create_invitation_team_not_found(self, service, mock_db):
        """Test creating invitation for non-existent team."""
        mock_query = MagicMock()
        mock_query.filter.return_value.first.return_value = None
        mock_db.query.return_value = mock_query

        result = await service.create_invitation(team_id="nonexistent", email="user@example.com", role="member", invited_by="admin@example.com")

        assert result is None

    @pytest.mark.asyncio
    async def test_create_invitation_personal_team_rejected(self, service, mock_team, mock_db):
        """Test creating invitation for personal team is rejected."""
        mock_team.is_personal = True

        mock_query = MagicMock()
        mock_query.filter.return_value.first.return_value = mock_team
        mock_db.query.return_value = mock_query

        with pytest.raises(ValueError, match="Cannot send invitations to personal teams"):
            await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

    @pytest.mark.asyncio
    async def test_create_invitation_inviter_not_found(self, service, mock_team, mock_db):
        """Test creating invitation with non-existent inviter."""

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                mock_query.filter.return_value.first.return_value = None
            return mock_query

        mock_db.query.side_effect = query_side_effect

        result = await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="nonexistent@example.com")

        assert result is None

    @pytest.mark.asyncio
    async def test_create_invitation_inviter_not_member(self, service, mock_team, mock_inviter, mock_db):
        """Test creating invitation when inviter is not a team member."""

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                mock_query.filter.return_value.first.return_value = None
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with pytest.raises(ValueError, match="Only team members can send invitations"):
            await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

    @pytest.mark.asyncio
    async def test_create_invitation_inviter_insufficient_permissions(self, service, mock_team, mock_inviter, mock_membership, mock_db):
        """Test creating invitation when inviter lacks permissions."""
        mock_membership.role = "member"  # Not owner

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                mock_query.filter.return_value.first.return_value = mock_membership
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with pytest.raises(ValueError, match="Only team owners can send invitations"):
            await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

    @pytest.mark.asyncio
    async def test_create_invitation_user_already_member(self, service, mock_team, mock_inviter, mock_membership, mock_db):
        """Test creating invitation for user who is already a member."""
        existing_member = MagicMock(spec=EmailTeamMember)
        existing_member.is_active = True

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                if not hasattr(query_side_effect, "call_count"):
                    query_side_effect.call_count = 0
                query_side_effect.call_count += 1

                if query_side_effect.call_count == 1:
                    mock_query.filter.return_value.first.return_value = mock_membership
                else:
                    mock_query.filter.return_value.first.return_value = existing_member
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with pytest.raises(ValueError, match="already a member of this team"):
            await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

    @pytest.mark.asyncio
    async def test_create_invitation_active_invitation_exists(self, service, mock_team, mock_inviter, mock_membership, mock_invitation, mock_db):
        """Test creating invitation when active invitation already exists."""

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                if not hasattr(query_side_effect, "member_call_count"):
                    query_side_effect.member_call_count = 0
                query_side_effect.member_call_count += 1

                if query_side_effect.member_call_count == 1:
                    mock_query.filter.return_value.first.return_value = mock_membership
                else:
                    mock_query.filter.return_value.first.return_value = None
            elif model == EmailTeamInvitation:
                mock_query.filter.return_value.first.return_value = mock_invitation
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with pytest.raises(ValueError, match="An active invitation already exists"):
            await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

    @pytest.mark.asyncio
    async def test_create_invitation_max_members_exceeded(self, service, mock_team, mock_inviter, mock_membership, mock_db):
        """Test creating invitation when team has reached max members."""
        mock_team.max_members = 10

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                if not hasattr(query_side_effect, "member_call_count"):
                    query_side_effect.member_call_count = 0
                query_side_effect.member_call_count += 1

                if query_side_effect.member_call_count == 1:
                    mock_query.filter.return_value.first.return_value = mock_membership
                elif query_side_effect.member_call_count == 2:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 8
            elif model == EmailTeamInvitation:
                if not hasattr(query_side_effect, "invitation_call_count"):
                    query_side_effect.invitation_call_count = 0
                query_side_effect.invitation_call_count += 1

                if query_side_effect.invitation_call_count == 1:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 2  # 8 + 2 = 10, at limit
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with pytest.raises(TeamMemberLimitExceededError, match="maximum member limit"):
            await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

    @pytest.mark.asyncio
    async def test_create_invitation_null_max_members_uses_global_default(self, service, mock_team, mock_inviter, mock_membership, mock_db):
        """Test that create_invitation falls back to settings.max_members_per_team when team.max_members is None."""
        mock_team.max_members = None  # no per-team override

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                if not hasattr(query_side_effect, "member_call_count"):
                    query_side_effect.member_call_count = 0
                query_side_effect.member_call_count += 1

                if query_side_effect.member_call_count == 1:
                    mock_query.filter.return_value.first.return_value = mock_membership
                elif query_side_effect.member_call_count == 2:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 8
            elif model == EmailTeamInvitation:
                if not hasattr(query_side_effect, "invitation_call_count"):
                    query_side_effect.invitation_call_count = 0
                query_side_effect.invitation_call_count += 1

                if query_side_effect.invitation_call_count == 1:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 2  # 8 + 2 = 10
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with patch("mcpgateway.services.team_management_service.settings") as mock_settings:
            mock_settings.max_members_per_team = 10  # global limit matches total (8+2)
            with pytest.raises(TeamMemberLimitExceededError, match="maximum member limit"):
                await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

    # =========================================================================
    # Invitation Retrieval Tests
    # =========================================================================

    @pytest.mark.asyncio
    async def test_get_invitation_by_token_found(self, service, mock_db, mock_invitation):
        """Test getting invitation by token when invitation exists."""
        mock_query = MagicMock()
        mock_query.filter.return_value.first.return_value = mock_invitation
        mock_db.query.return_value = mock_query

        result = await service.get_invitation_by_token("secure_token_123")

        assert result == mock_invitation
        mock_db.query.assert_called_once_with(EmailTeamInvitation)

    @pytest.mark.asyncio
    async def test_get_invitation_by_token_not_found(self, service, mock_db):
        """Test getting invitation by token when invitation doesn't exist."""
        mock_query = MagicMock()
        mock_query.filter.return_value.first.return_value = None
        mock_db.query.return_value = mock_query

        result = await service.get_invitation_by_token("nonexistent_token")

        assert result is None

    @pytest.mark.asyncio
    async def test_get_invitation_by_token_database_error(self, service, mock_db):
        """Test getting invitation by token with database error."""
        mock_db.query.side_effect = Exception("Database error")

        result = await service.get_invitation_by_token("token")

        assert result is None

    # =========================================================================
    # Invitation Acceptance Tests
    # =========================================================================

    @pytest.mark.skip("Complex integration test - main functionality covered by simpler tests")
    @pytest.mark.asyncio
    async def test_accept_invitation_success(self, service, mock_db):
        """Test successful invitation acceptance."""
        # Create fresh mocks
        mock_invitation = MagicMock(spec=EmailTeamInvitation)
        mock_invitation.team_id = "team123"
        mock_invitation.email = "user@example.com"
        mock_invitation.role = "member"
        mock_invitation.is_valid.return_value = True
        mock_invitation.is_active = True

        mock_team = MagicMock(spec=EmailTeam)
        mock_team.max_members = 100

        call_counts = {"team": 0, "member": 0}

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                call_counts["team"] += 1
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailTeamMember:
                call_counts["member"] += 1
                if call_counts["member"] == 1:
                    # Check if user is already a member
                    mock_query.filter.return_value.first.return_value = None
                else:
                    # Member count check
                    mock_query.filter.return_value.count.return_value = 5
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with (
            patch.object(service, "get_invitation_by_token", return_value=mock_invitation),
            patch("mcpgateway.services.team_invitation_service.EmailTeamMember") as MockMember,
            patch("mcpgateway.services.team_invitation_service.utc_now"),
        ):
            mock_membership_instance = MagicMock()
            MockMember.return_value = mock_membership_instance

            result = await service.accept_invitation("secure_token_123")

            assert result is mock_membership_instance
            assert mock_invitation.is_active is False
            mock_db.add.assert_called_once_with(mock_membership_instance)
            mock_db.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_accept_invitation_not_found(self, service):
        """Test accepting non-existent invitation."""
        with patch.object(service, "get_invitation_by_token", return_value=None):
            with pytest.raises(ValueError, match="Invitation not found"):
                await service.accept_invitation("nonexistent_token")

    @pytest.mark.asyncio
    async def test_accept_invitation_invalid(self, service, mock_invitation):
        """Test accepting invalid/expired invitation."""
        mock_invitation.is_valid.return_value = False

        with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
            with pytest.raises(ValueError, match="Invitation is invalid or expired"):
                await service.accept_invitation("expired_token")

    @pytest.mark.asyncio
    async def test_accept_invitation_email_mismatch(self, service, mock_invitation):
        """Test accepting invitation with mismatched email."""
        with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
            with pytest.raises(ValueError, match="Email address does not match"):
                await service.accept_invitation("token", accepting_user_email="wrong@example.com")

    @pytest.mark.asyncio
    async def test_accept_invitation_user_not_found(self, service, mock_invitation, mock_db):
        """Test accepting invitation when user doesn't exist."""
        mock_query = MagicMock()
        mock_query.filter.return_value.first.return_value = None
        mock_db.query.return_value = mock_query

        with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
            with pytest.raises(ValueError, match="User account not found"):
                await service.accept_invitation("token", accepting_user_email="user@example.com")

    @pytest.mark.asyncio
    async def test_accept_invitation_team_not_found(self, service, mock_invitation, mock_db):
        """Test accepting invitation when team no longer exists."""
        mock_user = MagicMock(spec=EmailUser)

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailUser:
                mock_query.filter.return_value.first.return_value = mock_user
            elif model == EmailTeam:
                mock_query.filter.return_value.first.return_value = None
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
            with pytest.raises(ValueError, match="Team not found or inactive"):
                await service.accept_invitation("token", accepting_user_email="user@example.com")

    @pytest.mark.asyncio
    async def test_accept_invitation_already_member(self, service, mock_invitation, mock_team, mock_db):
        """Test accepting invitation when user is already a member."""
        existing_member = MagicMock(spec=EmailTeamMember)
        existing_member.is_active = True

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailTeamMember:
                mock_query.filter.return_value.first.return_value = existing_member
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
            with pytest.raises(ValueError, match="already a member of this team"):
                await service.accept_invitation("token")

            # Should deactivate the invitation
            assert mock_invitation.is_active is False
            mock_db.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_accept_invitation_team_full(self, service, mock_invitation, mock_team, mock_db):
        """Test accepting invitation when team is at capacity."""
        mock_team.max_members = 10

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailTeamMember:
                if not hasattr(query_side_effect, "call_count"):
                    query_side_effect.call_count = 0
                query_side_effect.call_count += 1

                if query_side_effect.call_count == 1:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 10
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
            with pytest.raises(TeamMemberLimitExceededError, match="maximum member limit"):
                await service.accept_invitation("token")

    @pytest.mark.asyncio
    async def test_accept_invitation_null_max_members_uses_global_default(self, service, mock_invitation, mock_team, mock_db):
        """Test that accept_invitation falls back to settings.max_members_per_team when team.max_members is None."""
        mock_team.max_members = None  # no per-team override

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailTeamMember:
                if not hasattr(query_side_effect, "call_count"):
                    query_side_effect.call_count = 0
                query_side_effect.call_count += 1

                if query_side_effect.call_count == 1:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 10
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with patch("mcpgateway.services.team_management_service.settings") as mock_settings:
            mock_settings.max_members_per_team = 10  # global limit matches count
            with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
                with pytest.raises(TeamMemberLimitExceededError, match="maximum member limit"):
                    await service.accept_invitation("token")

    # =========================================================================
    # Invitation Decline Tests
    # =========================================================================

    @pytest.mark.asyncio
    async def test_decline_invitation_success(self, service, mock_db, mock_invitation):
        """Test successful invitation decline."""
        with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
            result = await service.decline_invitation("secure_token_123")

            assert result is True
            assert mock_invitation.is_active is False
            mock_db.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_decline_invitation_not_found(self, service):
        """Test declining non-existent invitation."""
        with patch.object(service, "get_invitation_by_token", return_value=None):
            result = await service.decline_invitation("nonexistent_token")

            assert result is False

    @pytest.mark.asyncio
    async def test_decline_invitation_email_mismatch(self, service, mock_invitation):
        """Test declining invitation with mismatched email."""
        with patch.object(service, "get_invitation_by_token", return_value=mock_invitation):
            result = await service.decline_invitation("token", declining_user_email="wrong@example.com")

            assert result is False

    # =========================================================================
    # Invitation Revocation Tests
    # =========================================================================

    @pytest.mark.asyncio
    async def test_revoke_invitation_success(self, service, mock_db, mock_invitation, mock_membership):
        """Test successful invitation revocation."""

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeamInvitation:
                mock_query.filter.return_value.first.return_value = mock_invitation
            elif model == EmailTeamMember:
                mock_query.filter.return_value.first.return_value = mock_membership
            return mock_query

        mock_db.query.side_effect = query_side_effect

        result = await service.revoke_invitation("invite123", "admin@example.com")

        assert result is True
        assert mock_invitation.is_active is False
        mock_db.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_revoke_invitation_not_found(self, service, mock_db):
        """Test revoking non-existent invitation."""
        mock_query = MagicMock()
        mock_query.filter.return_value.first.return_value = None
        mock_db.query.return_value = mock_query

        result = await service.revoke_invitation("nonexistent", "admin@example.com")

        assert result is False

    @pytest.mark.asyncio
    async def test_revoke_invitation_insufficient_permissions(self, service, mock_db, mock_invitation):
        """Test revoking invitation without permissions."""
        mock_membership = MagicMock(spec=EmailTeamMember)
        mock_membership.role = "member"  # Not admin or owner

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeamInvitation:
                mock_query.filter.return_value.first.return_value = mock_invitation
            elif model == EmailTeamMember:
                mock_query.filter.return_value.first.return_value = mock_membership
            return mock_query

        mock_db.query.side_effect = query_side_effect

        result = await service.revoke_invitation("invite123", "member@example.com")

        assert result is False

    # =========================================================================
    # Invitation Listing Tests
    # =========================================================================

    @pytest.mark.asyncio
    async def test_get_team_invitations(self, service, mock_db):
        """Test getting team invitations."""
        mock_invitations = [MagicMock(spec=EmailTeamInvitation) for _ in range(3)]

        mock_query = MagicMock()
        mock_query.filter.return_value.filter.return_value.order_by.return_value.all.return_value = mock_invitations
        mock_db.query.return_value = mock_query

        result = await service.get_team_invitations("team123")

        assert result == mock_invitations
        mock_db.query.assert_called_once_with(EmailTeamInvitation)

    @pytest.mark.asyncio
    async def test_get_team_invitations_include_inactive(self, service, mock_db):
        """Test getting team invitations including inactive ones."""
        mock_invitations = [MagicMock(spec=EmailTeamInvitation) for _ in range(5)]

        mock_query = MagicMock()
        mock_query.filter.return_value.order_by.return_value.all.return_value = mock_invitations
        mock_db.query.return_value = mock_query

        result = await service.get_team_invitations("team123", active_only=False)

        assert result == mock_invitations

    @pytest.mark.asyncio
    async def test_get_user_invitations(self, service, mock_db):
        """Test getting user invitations."""
        mock_invitations = [MagicMock(spec=EmailTeamInvitation) for _ in range(2)]

        mock_query = MagicMock()
        mock_query.filter.return_value.filter.return_value.order_by.return_value.all.return_value = mock_invitations
        mock_db.query.return_value = mock_query

        result = await service.get_user_invitations("user@example.com")

        assert result == mock_invitations
        mock_db.query.assert_called_once_with(EmailTeamInvitation)

    # =========================================================================
    # Invitation Cleanup Tests
    # =========================================================================

    @pytest.mark.asyncio
    async def test_cleanup_expired_invitations(self, service, mock_db):
        """Test cleanup of expired invitations."""
        mock_query = MagicMock()
        mock_query.filter.return_value.update.return_value = 5
        mock_db.query.return_value = mock_query

        result = await service.cleanup_expired_invitations()

        assert result == 5
        mock_db.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_cleanup_expired_invitations_none_expired(self, service, mock_db):
        """Test cleanup when no invitations are expired."""
        mock_query = MagicMock()
        mock_query.filter.return_value.update.return_value = 0
        mock_db.query.return_value = mock_query

        result = await service.cleanup_expired_invitations()

        assert result == 0
        mock_db.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_cleanup_expired_invitations_database_error(self, service, mock_db):
        """Test cleanup with database error."""
        mock_db.query.side_effect = Exception("Database error")

        result = await service.cleanup_expired_invitations()

        assert result == 0
        mock_db.rollback.assert_called_once()

    # =========================================================================
    # Error Handling Tests
    # =========================================================================

    @pytest.mark.asyncio
    async def test_database_error_handling(self, service, mock_db):
        """Test various database error scenarios return appropriate defaults."""
        mock_db.query.side_effect = Exception("Database connection failed")

        # Test methods that should return None on error
        assert await service.get_invitation_by_token("token") is None

        # Test methods that should return empty lists on error
        assert await service.get_team_invitations("team123") == []
        assert await service.get_user_invitations("user@example.com") == []

        # Test cleanup returns 0 on error
        assert await service.cleanup_expired_invitations() == 0

    @pytest.mark.asyncio
    async def test_rollback_on_errors(self, service, mock_db):
        """Test that database rollback is called on errors."""
        # Test create_invitation rollback
        mock_db.add.side_effect = Exception("Database error")

        with patch("mcpgateway.services.team_invitation_service.EmailTeamInvitation"):
            try:
                await service.create_invitation("team", "email", "member", "inviter")
            except Exception:
                pass

            mock_db.rollback.assert_called()

    # =========================================================================
    # Edge Case Tests
    # =========================================================================

    @pytest.mark.skip("Complex integration test - main functionality covered by simpler tests")
    @pytest.mark.asyncio
    async def test_deactivate_existing_invitation_before_creating_new(self, service, mock_db):
        """Test that existing expired invitations are deactivated before creating new ones."""
        # Create fresh mocks
        mock_team = MagicMock(spec=EmailTeam)
        mock_team.is_personal = False
        mock_team.max_members = 100

        mock_inviter = MagicMock(spec=EmailUser)
        mock_membership = MagicMock(spec=EmailTeamMember)
        mock_membership.role = "owner"

        mock_invitation = MagicMock(spec=EmailTeamInvitation)
        mock_invitation.is_expired.return_value = True
        mock_invitation.is_active = True

        call_counts = {"team": 0, "user": 0, "member": 0, "invitation": 0}

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                call_counts["team"] += 1
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                call_counts["user"] += 1
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                call_counts["member"] += 1
                if call_counts["member"] == 1:
                    mock_query.filter.return_value.first.return_value = mock_membership
                elif call_counts["member"] == 2:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 5
            elif model == EmailTeamInvitation:
                call_counts["invitation"] += 1
                if call_counts["invitation"] == 1:
                    mock_query.filter.return_value.first.return_value = mock_invitation
                else:
                    mock_query.filter.return_value.count.return_value = 2
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with (
            patch("mcpgateway.services.team_invitation_service.EmailTeamInvitation") as MockInvitation,
            patch("mcpgateway.services.team_invitation_service.utc_now"),
            patch("mcpgateway.services.team_invitation_service.timedelta"),
        ):
            mock_new_invitation = MagicMock()
            MockInvitation.return_value = mock_new_invitation

            result = await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

            # Should deactivate existing invitation and create new one
            assert mock_invitation.is_active is False
            assert result == mock_new_invitation

    def test_role_validation_values(self, service):
        """Test that role validation accepts all valid values."""
        valid_roles = ["owner", "member"]

        for role in valid_roles:
            # Should not raise an exception during validation
            # This is tested implicitly in create_invitation tests
            assert role in valid_roles

    @pytest.mark.skip("Complex integration test - main functionality covered by simpler tests")
    @pytest.mark.asyncio
    async def test_expiry_days_from_settings(self, service, mock_db):
        """Test that invitation expiry uses settings default."""
        # Create fresh mocks
        mock_team = MagicMock(spec=EmailTeam)
        mock_team.is_personal = False
        mock_team.max_members = 100

        mock_inviter = MagicMock(spec=EmailUser)
        mock_membership = MagicMock(spec=EmailTeamMember)
        mock_membership.role = "owner"

        call_counts = {"team": 0, "user": 0, "member": 0, "invitation": 0}

        def query_side_effect(model):
            mock_query = MagicMock()
            if model == EmailTeam:
                call_counts["team"] += 1
                mock_query.filter.return_value.first.return_value = mock_team
            elif model == EmailUser:
                call_counts["user"] += 1
                mock_query.filter.return_value.first.return_value = mock_inviter
            elif model == EmailTeamMember:
                call_counts["member"] += 1
                if call_counts["member"] == 1:
                    mock_query.filter.return_value.first.return_value = mock_membership
                elif call_counts["member"] == 2:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 5
            elif model == EmailTeamInvitation:
                call_counts["invitation"] += 1
                if call_counts["invitation"] == 1:
                    mock_query.filter.return_value.first.return_value = None
                else:
                    mock_query.filter.return_value.count.return_value = 2
            return mock_query

        mock_db.query.side_effect = query_side_effect

        with (
            patch("mcpgateway.services.team_invitation_service.settings") as mock_settings,
            patch("mcpgateway.services.team_invitation_service.EmailTeamInvitation") as MockInvitation,
            patch("mcpgateway.services.team_invitation_service.utc_now"),
            patch("mcpgateway.services.team_invitation_service.timedelta"),
        ):
            mock_settings.invitation_expiry_days = 14
            mock_invitation_instance = MagicMock()
            MockInvitation.return_value = mock_invitation_instance

            await service.create_invitation(team_id="team123", email="user@example.com", role="member", invited_by="admin@example.com")

            # Should use settings default for expiry
            MockInvitation.assert_called_once()
            call_kwargs = MockInvitation.call_args[1]
            # Check that expires_at was set (we can't easily check the exact value due to datetime)
            assert "expires_at" in call_kwargs
