"""Unit tests for CLI users commands."""

from __future__ import annotations

import json
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from tests.conftest import CliRunner

from teams_phone.cli.main import app
from teams_phone.exceptions import NotFoundError, ValidationError
from teams_phone.models import (
    AccountType,
    EffectivePolicyAssignment,
    PolicyAssignmentDetail,
    TelephoneNumber,
    UserConfiguration,
)

if TYPE_CHECKING:
    pass

runner = CliRunner()


# Test fixtures
def _make_user(
    user_id: str = "11111111-1111-1111-1111-111111111111",
    upn: str = "john.doe@contoso.com",
    display_name: str = "John Doe",
    account_type: AccountType = AccountType.USER,
    is_enterprise_voice_enabled: bool = True,
    phone_number: str | None = "+14255551234",
) -> UserConfiguration:
    """Create a UserConfiguration for testing."""
    telephone_numbers = []
    if phone_number:
        telephone_numbers.append(
            TelephoneNumber(
                telephone_number=phone_number,
                assignment_category="primary",
            )
        )

    return UserConfiguration(
        id=user_id,
        user_principal_name=upn,
        tenant_id="tenant-123",
        display_name=display_name,
        account_type=account_type,
        is_enterprise_voice_enabled=is_enterprise_voice_enabled,
        telephone_numbers=telephone_numbers,
        feature_types=["Teams", "CallingPlan"] if is_enterprise_voice_enabled else [],
        effective_policy_assignments=[
            EffectivePolicyAssignment(
                policy_type="TeamsCallingPolicy",
                policy_assignment=PolicyAssignmentDetail(
                    display_name="AllowCalling",
                    assignment_type="direct",
                    policy_id="policy-123",
                    group_id=None,
                ),
            ),
        ],
    )


def _mock_async_context():
    """Create mock patches for async service execution."""
    return (
        patch("teams_phone.cli.users.CacheManager"),
        patch("teams_phone.cli.users.AuthService"),
        patch("teams_phone.cli.users.GraphClient"),
        patch("teams_phone.cli.users.UserService"),
    )


class TestUsersListCommand:
    """Tests for users list command."""

    @pytest.mark.unit
    def test_list_shows_in_help(self) -> None:
        """list command appears in users help."""
        result = runner.invoke(app, ["users", "--help"])
        assert result.exit_code == 0
        assert "list" in result.stdout

    @pytest.mark.unit
    def test_list_empty(self) -> None:
        """list shows empty message when no users found."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list"])
            assert result.exit_code == 0
            assert "No users found" in result.stdout

    @pytest.mark.unit
    def test_list_shows_users(self) -> None:
        """list shows users in table format."""
        users = [
            _make_user(upn="john.doe@contoso.com", display_name="John Doe"),
            _make_user(upn="jane.smith@contoso.com", display_name="Jane Smith"),
        ]

        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=users)
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["--no-color", "users", "list"])
            assert result.exit_code == 0
            assert "John Doe" in result.stdout
            assert "Jane Smith" in result.stdout
            # Rich tables may truncate columns, so check partial match
            assert "john.doe" in result.stdout

    @pytest.mark.unit
    def test_list_json_output(self) -> None:
        """list outputs valid JSON with --json flag."""
        users = [_make_user()]

        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=users)
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "users", "list"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 1
            assert data[0]["user_principal_name"] == "john.doe@contoso.com"
            assert data[0]["display_name"] == "John Doe"
            assert data[0]["is_enterprise_voice_enabled"] is True

    @pytest.mark.unit
    def test_list_json_empty(self) -> None:
        """list outputs empty JSON array when no users."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "users", "list"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data == []

    @pytest.mark.unit
    def test_list_with_licensed_filter(self) -> None:
        """list passes licensed filter to service."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list", "--licensed"])
            assert result.exit_code == 0
            mock_service.list_users.assert_called_once()
            call_kwargs = mock_service.list_users.call_args[1]
            assert call_kwargs["licensed"] is True

    @pytest.mark.unit
    def test_list_with_unlicensed_filter(self) -> None:
        """list passes unlicensed filter to service."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list", "--unlicensed"])
            assert result.exit_code == 0
            mock_service.list_users.assert_called_once()
            call_kwargs = mock_service.list_users.call_args[1]
            assert call_kwargs["licensed"] is False

    @pytest.mark.unit
    def test_list_with_assigned_filter(self) -> None:
        """list passes assigned filter to service."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list", "--assigned"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_users.call_args[1]
            assert call_kwargs["assigned"] is True

    @pytest.mark.unit
    def test_list_with_unassigned_filter(self) -> None:
        """list passes unassigned filter to service."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list", "--unassigned"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_users.call_args[1]
            assert call_kwargs["assigned"] is False

    @pytest.mark.unit
    def test_list_with_account_type_filter(self) -> None:
        """list passes account type filter to service."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list", "--account-type", "user"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_users.call_args[1]
            assert call_kwargs["account_type"] == AccountType.USER

    @pytest.mark.unit
    def test_list_with_limit(self) -> None:
        """list passes limit to service."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list", "--limit", "10"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_users.call_args[1]
            assert call_kwargs["max_items"] == 10

    @pytest.mark.unit
    def test_list_with_all_flag(self) -> None:
        """list passes None max_items for --all flag."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list", "--all"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_users.call_args[1]
            assert call_kwargs["max_items"] is None

    @pytest.mark.unit
    def test_list_limit_and_all_mutually_exclusive(self) -> None:
        """list errors when both --limit and --all are specified."""
        result = runner.invoke(app, ["users", "list", "--limit", "10", "--all"])
        assert result.exit_code == 5  # ValidationError exit code
        assert "Cannot specify both --limit and --all" in result.stdout

    @pytest.mark.unit
    def test_list_default_limit(self) -> None:
        """list uses default limit of 50 when no limit specified."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.list_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "list"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_users.call_args[1]
            assert call_kwargs["max_items"] == 50


class TestUsersShowCommand:
    """Tests for users show command."""

    @pytest.mark.unit
    def test_show_shows_in_help(self) -> None:
        """show command appears in users help."""
        result = runner.invoke(app, ["users", "--help"])
        assert result.exit_code == 0
        assert "show" in result.stdout

    @pytest.mark.unit
    def test_show_requires_user_argument(self) -> None:
        """show requires user argument."""
        result = runner.invoke(app, ["users", "show"])
        assert result.exit_code == 2
        # Typer outputs error to stderr, combined output is in result.output
        output = result.stdout + (result.stderr or "")
        assert "USER" in output or "Missing argument" in output or result.exit_code == 2

    @pytest.mark.unit
    def test_show_displays_user(self) -> None:
        """show displays user details."""
        user = _make_user()

        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.resolve_user = AsyncMock(return_value=user)
            mock_us_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "users", "show", "john.doe@contoso.com"]
            )
            assert result.exit_code == 0
            assert "John Doe" in result.stdout
            assert "john.doe@contoso.com" in result.stdout
            assert (
                "14255551234" in result.stdout
            )  # Without + to avoid ANSI escape issues

    @pytest.mark.unit
    def test_show_json_output(self) -> None:
        """show outputs valid JSON with --json flag."""
        user = _make_user()

        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.resolve_user = AsyncMock(return_value=user)
            mock_us_class.return_value = mock_service

            result = runner.invoke(
                app, ["--json", "users", "show", "john.doe@contoso.com"]
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["user_principal_name"] == "john.doe@contoso.com"
            assert data["display_name"] == "John Doe"
            assert len(data["telephone_numbers"]) == 1
            assert data["telephone_numbers"][0]["telephone_number"] == "+14255551234"

    @pytest.mark.unit
    def test_show_not_found(self) -> None:
        """show exits with error when user not found."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.resolve_user = AsyncMock(
                side_effect=NotFoundError(
                    "User 'nonexistent@contoso.com' not found",
                    remediation="Verify the user principal name is correct.",
                )
            )
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "show", "nonexistent@contoso.com"])
            assert result.exit_code == 4  # NotFoundError exit code

    @pytest.mark.unit
    def test_show_ambiguous_user(self) -> None:
        """show exits with error when multiple users match."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.resolve_user = AsyncMock(
                side_effect=ValidationError(
                    "Multiple users match 'John'",
                    remediation="Please specify the user by their full UPN.",
                )
            )
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "show", "John"])
            assert result.exit_code == 5  # ValidationError exit code

    @pytest.mark.unit
    def test_show_displays_no_phone_number(self) -> None:
        """show displays dash when user has no phone number."""
        user = _make_user(phone_number=None)

        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.resolve_user = AsyncMock(return_value=user)
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "show", "john.doe@contoso.com"])
            assert result.exit_code == 0
            assert "No phone number assigned" in result.stdout


class TestUsersSearchCommand:
    """Tests for users search command."""

    @pytest.mark.unit
    def test_search_shows_in_help(self) -> None:
        """search command appears in users help."""
        result = runner.invoke(app, ["users", "--help"])
        assert result.exit_code == 0
        assert "search" in result.stdout

    @pytest.mark.unit
    def test_search_requires_query_argument(self) -> None:
        """search requires query argument."""
        result = runner.invoke(app, ["users", "search"])
        assert result.exit_code == 2
        # Typer outputs error to stderr, combined output is in result.output
        output = result.stdout + (result.stderr or "")
        assert (
            "QUERY" in output or "Missing argument" in output or result.exit_code == 2
        )

    @pytest.mark.unit
    def test_search_shows_results(self) -> None:
        """search shows matching users."""
        users = [
            _make_user(upn="john.doe@contoso.com", display_name="John Doe"),
            _make_user(upn="john.smith@contoso.com", display_name="John Smith"),
        ]

        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.search_users = AsyncMock(return_value=users)
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "search", "John"])
            assert result.exit_code == 0
            assert "John Doe" in result.stdout
            assert "John Smith" in result.stdout

    @pytest.mark.unit
    def test_search_no_results(self) -> None:
        """search shows message when no users found."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.search_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "users", "search", "nonexistent"]
            )
            assert result.exit_code == 0
            assert "No users found matching" in result.stdout
            assert "nonexistent" in result.stdout

    @pytest.mark.unit
    def test_search_json_output(self) -> None:
        """search outputs valid JSON with --json flag."""
        users = [_make_user()]

        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.search_users = AsyncMock(return_value=users)
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "users", "search", "John"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 1
            assert data[0]["display_name"] == "John Doe"

    @pytest.mark.unit
    def test_search_with_limit(self) -> None:
        """search passes limit to service."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.search_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "search", "John", "--limit", "10"])
            assert result.exit_code == 0
            mock_service.search_users.assert_called_once()
            call_kwargs = mock_service.search_users.call_args[1]
            assert call_kwargs["max_results"] == 10

    @pytest.mark.unit
    def test_search_default_limit(self) -> None:
        """search uses default limit of 25."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.search_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "search", "John"])
            assert result.exit_code == 0
            call_kwargs = mock_service.search_users.call_args[1]
            assert call_kwargs["max_results"] == 25

    @pytest.mark.unit
    def test_search_with_field_name(self) -> None:
        """search with --field name passes query to service."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.search_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(app, ["users", "search", "John", "--field", "name"])
            assert result.exit_code == 0
            mock_service.search_users.assert_called_once()
            call_args = mock_service.search_users.call_args
            assert call_args[0][0] == "John"

    @pytest.mark.unit
    def test_search_with_field_phone(self) -> None:
        """search with --field phone prepends + for numeric query."""
        with (
            patch("teams_phone.cli.users.CacheManager"),
            patch("teams_phone.cli.users.AuthService"),
            patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.users.UserService") as mock_us_class,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_service = MagicMock()
            mock_service.search_users = AsyncMock(return_value=[])
            mock_us_class.return_value = mock_service

            result = runner.invoke(
                app, ["users", "search", "14255551234", "--field", "phone"]
            )
            assert result.exit_code == 0
            mock_service.search_users.assert_called_once()
            call_args = mock_service.search_users.call_args
            assert call_args[0][0] == "+14255551234"


class TestUsersCommandIntegration:
    """Integration tests for users command group."""

    @pytest.mark.unit
    def test_users_commands_appear_in_help(self) -> None:
        """All users commands appear in users --help."""
        result = runner.invoke(app, ["users", "--help"])
        assert result.exit_code == 0
        assert "list" in result.stdout
        assert "show" in result.stdout
        assert "search" in result.stdout

    @pytest.mark.unit
    def test_users_no_args_shows_help(self) -> None:
        """users with no args shows help (exit code 2)."""
        result = runner.invoke(app, ["users"])
        assert result.exit_code == 2
        assert "Usage:" in result.stdout

    @pytest.mark.unit
    def test_users_appears_in_main_help(self) -> None:
        """users command group appears in main help."""
        result = runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        assert "users" in result.stdout
