"""Unit tests for CLI numbers commands."""

from __future__ import annotations

import json
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any
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
from teams_phone.models import (
    AccountType,
    ActivationState,
    AssignmentStatus,
    AsyncOperation,
    EmergencyLocation,
    NumberAssignment,
    NumberType,
    OperationStatus,
    UserConfiguration,
)

if TYPE_CHECKING:
    from pathlib import Path

runner = CliRunner()


# Test fixtures
def _make_number(
    number_id: str = "num-123",
    telephone_number: str = "+14255551234",
    number_type: NumberType = NumberType.DIRECT_ROUTING,
    assignment_status: AssignmentStatus = AssignmentStatus.UNASSIGNED,
    activation_state: ActivationState = ActivationState.ACTIVATED,
    city: str | None = "Seattle",
    location_id: str | None = None,
    assignment_target_id: str | None = None,
) -> NumberAssignment:
    """Create a NumberAssignment for testing."""
    # Use model_validate with dict to leverage populate_by_name
    return NumberAssignment.model_validate(
        {
            "id": number_id,
            "telephoneNumber": telephone_number,
            "numberType": number_type.value,
            "assignmentStatus": assignment_status.value,
            "activationState": activation_state.value,
            "capabilities": ["userAssignment"],
            "city": city,
            "locationId": location_id,
            "assignmentTargetId": assignment_target_id,
        }
    )


class TestNumbersListCommand:
    """Tests for numbers list command."""

    @pytest.mark.unit
    def test_list_shows_in_help(self) -> None:
        """list command appears in numbers help."""
        result = runner.invoke(app, ["numbers", "--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 numbers found."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

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

    @pytest.mark.unit
    def test_list_shows_numbers(self) -> None:
        """list shows numbers in table format."""
        numbers = [
            _make_number(telephone_number="+14255551001", city="Seattle"),
            _make_number(telephone_number="+14255551002", city="Portland"),
        ]

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=numbers)
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["--no-color", "numbers", "list"])
            assert result.exit_code == 0
            assert "+14255551001" in result.stdout
            assert "+14255551002" in result.stdout
            assert "Seattle" in result.stdout
            assert "Portland" in result.stdout

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

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=numbers)
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "numbers", "list"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 1
            assert data[0]["telephone_number"] == "+14255551234"
            assert data[0]["number_type"] == "directRouting"
            assert data[0]["assignment_status"] == "unassigned"

    @pytest.mark.unit
    def test_list_json_empty(self) -> None:
        """list outputs empty JSON array when no numbers."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

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

    @pytest.mark.unit
    def test_list_with_status_filter(self) -> None:
        """list passes status filter to service."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "list", "--status", "unassigned"])
            assert result.exit_code == 0
            mock_service.list_numbers.assert_called_once()
            call_kwargs = mock_service.list_numbers.call_args[1]
            assert call_kwargs["status"] == AssignmentStatus.UNASSIGNED

    @pytest.mark.unit
    def test_list_with_type_filter(self) -> None:
        """list passes type filter to service."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "list", "--type", "callingPlan"])
            assert result.exit_code == 0
            mock_service.list_numbers.assert_called_once()
            call_kwargs = mock_service.list_numbers.call_args[1]
            assert call_kwargs["number_type"] == NumberType.CALLING_PLAN

    @pytest.mark.unit
    def test_list_with_city_filter(self) -> None:
        """list passes city filter to service."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "list", "--city", "Seattle"])
            assert result.exit_code == 0
            mock_service.list_numbers.assert_called_once()
            call_kwargs = mock_service.list_numbers.call_args[1]
            assert call_kwargs["city"] == "Seattle"

    @pytest.mark.unit
    def test_list_with_limit(self) -> None:
        """list passes limit to service."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "list", "--limit", "10"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_numbers.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.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "list", "--all"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_numbers.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, ["numbers", "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 100 when no limit specified."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "list"])
            assert result.exit_code == 0
            call_kwargs = mock_service.list_numbers.call_args[1]
            assert call_kwargs["max_items"] == 100


class TestNumbersShowCommand:
    """Tests for numbers show command."""

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

    @pytest.mark.unit
    def test_show_requires_number_argument(self) -> None:
        """show requires number argument."""
        result = runner.invoke(app, ["numbers", "show"])
        assert result.exit_code == 2
        output = result.stdout + (result.stderr or "")
        assert (
            "NUMBER" in output or "Missing argument" in output or result.exit_code == 2
        )

    @pytest.mark.unit
    def test_show_displays_number(self) -> None:
        """show displays number details."""
        number = _make_number(
            telephone_number="+14255551234",
            city="Seattle",
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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.get_number = AsyncMock(return_value=number)
            mock_ns_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "numbers", "show", "+14255551234"]
            )
            assert result.exit_code == 0
            # Phone numbers may have ANSI formatting, check for digits
            assert "14255551234" in result.stdout
            assert "Seattle" in result.stdout
            assert "Assigned" in result.stdout
            assert "user-" in result.stdout

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

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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.get_number = AsyncMock(return_value=number)
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "numbers", "show", "+14255551234"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["telephone_number"] == "+14255551234"
            assert data["number_type"] == "directRouting"
            assert data["city"] == "Seattle"

    @pytest.mark.unit
    def test_show_not_found(self) -> None:
        """show exits with error when number not found."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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.get_number = AsyncMock(
                side_effect=NotFoundError(
                    "Phone number '+19999999999' not found",
                    remediation="Verify the phone number is in your inventory.",
                )
            )
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "show", "+19999999999"])
            assert result.exit_code == 4  # NotFoundError exit code

    @pytest.mark.unit
    def test_show_displays_unassigned_number(self) -> None:
        """show displays unassigned number correctly."""
        number = _make_number(
            assignment_status=AssignmentStatus.UNASSIGNED,
            assignment_target_id=None,
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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.get_number = AsyncMock(return_value=number)
            mock_ns_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "numbers", "show", "+14255551234"]
            )
            assert result.exit_code == 0
            assert "Unassigned" in result.stdout

    @pytest.mark.unit
    def test_show_displays_calling_plan_type(self) -> None:
        """show displays calling plan number type correctly."""
        number = _make_number(number_type=NumberType.CALLING_PLAN)

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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.get_number = AsyncMock(return_value=number)
            mock_ns_class.return_value = mock_service

            result = runner.invoke(
                app, ["--no-color", "numbers", "show", "+14255551234"]
            )
            assert result.exit_code == 0
            assert "Calling Plan" in result.stdout


class TestNumbersSearchCommand:
    """Tests for numbers search command."""

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

    @pytest.mark.unit
    def test_search_requires_pattern_argument(self) -> None:
        """search requires pattern argument."""
        result = runner.invoke(app, ["numbers", "search"])
        assert result.exit_code == 2
        output = result.stdout + (result.stderr or "")
        assert (
            "PATTERN" 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 numbers."""
        numbers = [
            _make_number(telephone_number="+12065551001"),
            _make_number(telephone_number="+12065551002"),
        ]

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=numbers)
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["--no-color", "numbers", "search", "206*"])
            assert result.exit_code == 0
            assert "+12065551001" in result.stdout
            assert "+12065551002" in result.stdout
            assert "206*" in result.stdout  # Search pattern in title

    @pytest.mark.unit
    def test_search_no_results(self) -> None:
        """search shows message when no numbers found."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

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

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

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=numbers)
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["--json", "numbers", "search", "425*"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert len(data) == 1
            assert data[0]["telephone_number"] == "+14255551234"

    @pytest.mark.unit
    def test_search_with_limit(self) -> None:
        """search passes limit to service."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "search", "206*", "--limit", "10"])
            assert result.exit_code == 0
            mock_service.search_numbers.assert_called_once()
            call_kwargs = mock_service.search_numbers.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.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_numbers = AsyncMock(return_value=[])
            mock_ns_class.return_value = mock_service

            result = runner.invoke(app, ["numbers", "search", "206*"])
            assert result.exit_code == 0
            call_kwargs = mock_service.search_numbers.call_args[1]
            assert call_kwargs["max_results"] == 25


class TestNumbersCommandIntegration:
    """Integration tests for numbers command group."""

    @pytest.mark.unit
    def test_numbers_commands_appear_in_help(self) -> None:
        """All numbers commands appear in numbers --help."""
        result = runner.invoke(app, ["numbers", "--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_numbers_no_args_shows_help(self) -> None:
        """numbers with no args shows help (exit code 2)."""
        result = runner.invoke(app, ["numbers"])
        assert result.exit_code == 2
        assert "Usage:" in result.stdout

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


def _make_user(
    user_id: str = "user-123",
    upn: str = "john@contoso.com",
    display_name: str = "John Smith",
) -> UserConfiguration:
    """Create a UserConfiguration for testing."""
    return UserConfiguration.model_validate(
        {
            "id": user_id,
            "userPrincipalName": upn,
            "tenantId": "tenant-123",
            "displayName": display_name,
            "accountType": AccountType.USER.value,
            "isEnterpriseVoiceEnabled": True,
            "featureTypes": ["Teams"],
            "telephoneNumbers": [],
            "effectivePolicyAssignments": [],
        }
    )


def _make_location(
    location_id: str = "loc-123",
    description: str = "Seattle HQ",
) -> EmergencyLocation:
    """Create an EmergencyLocation for testing."""
    return EmergencyLocation.model_validate(
        {
            "locationId": location_id,
            "civicAddressId": "civic-123",
            "description": description,
            "isDefault": True,
        }
    )


def _make_operation(
    operation_id: str = "op-123",
    status: OperationStatus = OperationStatus.SUCCEEDED,
) -> AsyncOperation:
    """Create an AsyncOperation for testing."""
    return AsyncOperation.model_validate(
        {
            "id": operation_id,
            "createdDateTime": datetime.now(timezone.utc).isoformat(),
            "status": status.value,
            "numbers": [],
        }
    )


class TestNumbersAssignCommand:
    """Tests for numbers assign command."""

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

    @pytest.mark.unit
    def test_assign_requires_user_argument(self) -> None:
        """assign requires user argument."""
        result = runner.invoke(app, ["numbers", "assign"])
        assert result.exit_code == 2
        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_assign_requires_number_argument(self) -> None:
        """assign requires number argument."""
        result = runner.invoke(app, ["numbers", "assign", "john@contoso.com"])
        assert result.exit_code == 2
        output = result.stdout + (result.stderr or "")
        assert (
            "NUMBER" in output or "Missing argument" in output or result.exit_code == 2
        )

    @pytest.mark.unit
    def test_assign_dry_run_shows_preview(self) -> None:
        """assign --dry-run shows preview without executing."""
        mock_user = _make_user()
        mock_location = _make_location()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                    "--dry-run",
                ],
            )
            assert result.exit_code == 0
            assert "Dry Run Preview" in result.stdout
            assert "John Smith" in result.stdout
            # Phone numbers may have ANSI escape codes
            assert "14255551234" in result.stdout
            assert "Seattle HQ" in result.stdout
            assert "No changes made" in result.stdout

    @pytest.mark.unit
    def test_assign_dry_run_json_output(self) -> None:
        """assign --dry-run --json outputs valid JSON."""
        mock_user = _make_user()
        mock_location = _make_location()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            result = runner.invoke(
                app,
                [
                    "--json",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                    "--dry-run",
                ],
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["dry_run"] is True
            assert data["user_id"] == "user-123"
            assert data["user_principal_name"] == "john@contoso.com"
            assert data["phone_number"] == "+14255551234"
            assert data["number_type"] == "callingPlan"
            assert data["location_id"] == "loc-123"
            assert data["location_description"] == "Seattle HQ"

    @pytest.mark.unit
    def test_assign_calling_plan_without_location_fails(self) -> None:
        """assign Calling Plan number without location shows error."""
        mock_user = _make_user()
        mock_tenant = MagicMock()
        mock_tenant.default_location = None  # No default location

        # Create a mock config manager with get_default_tenant and get_tenant
        mock_config_manager = MagicMock()
        mock_config_manager.get_default_tenant = MagicMock(return_value="test-tenant")
        mock_config_manager.get_tenant = MagicMock(return_value=mock_tenant)

        # Create a mock CLIContext that returns our mock config_manager
        mock_cli_ctx = MagicMock()
        mock_cli_ctx.json_output = False
        mock_cli_ctx.get_config_manager = MagicMock(return_value=mock_config_manager)
        mock_cli_ctx.get_output_formatter = MagicMock(return_value=MagicMock())

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

            mock_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--force",
                ],
            )
            # ValidationError exit code - message goes to mock formatter
            assert result.exit_code == 5

    @pytest.mark.unit
    def test_assign_direct_routing_without_location_succeeds(self) -> None:
        """assign Direct Routing number without location succeeds."""
        mock_user = _make_user()
        mock_operation = _make_operation()

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

            mock_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_number_service = MagicMock()
            mock_number_service.assign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--type",
                    "directRouting",
                    "--force",
                ],
            )
            assert result.exit_code == 0
            assert "Assigned" in result.stdout

    @pytest.mark.unit
    def test_assign_success_json_output(self) -> None:
        """assign success outputs valid JSON."""
        mock_user = _make_user()
        mock_location = _make_location()
        mock_operation = _make_operation()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            mock_number_service = MagicMock()
            mock_number_service.assign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--json",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                ],
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "succeeded"
            assert data["user_id"] == "user-123"
            assert data["phone_number"] == "+14255551234"

    @pytest.mark.unit
    def test_assign_with_confirmation_cancelled(self) -> None:
        """assign cancelled by confirmation prompt exits gracefully."""
        mock_user = _make_user()
        mock_location = _make_location()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                ],
                input="n\n",  # Decline confirmation
            )
            assert result.exit_code == 0
            assert "cancelled" in result.stdout

    @pytest.mark.unit
    def test_assign_no_wait_shows_operation_id(self) -> None:
        """assign --no-wait shows operation ID."""
        mock_user = _make_user()
        mock_location = _make_location()
        mock_operation = _make_operation(status=OperationStatus.NOT_STARTED)

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            mock_number_service = MagicMock()
            mock_number_service.assign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                    "--force",
                    "--no-wait",
                ],
            )
            assert result.exit_code == 0
            # Operation ID may have ANSI escape codes
            assert "op-" in result.stdout
            assert "123" in result.stdout
            assert "initiated" in result.stdout

    @pytest.mark.unit
    def test_assign_user_not_found(self) -> None:
        """assign with unknown user shows error."""
        from teams_phone.exceptions import NotFoundError

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

            mock_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(
                side_effect=NotFoundError(
                    "User 'unknown@contoso.com' not found",
                    remediation="Verify the user exists.",
                )
            )
            mock_us_class.return_value = mock_user_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "assign",
                    "unknown@contoso.com",
                    "+14255551234",
                    "--type",
                    "directRouting",
                    "--force",
                ],
            )
            assert result.exit_code == 4  # NotFoundError exit code
            assert "not found" in result.stdout

    @pytest.mark.unit
    def test_assign_failed_operation_exits_with_error(self) -> None:
        """assign with failed operation exits with error code."""
        mock_user = _make_user()
        mock_location = _make_location()
        mock_operation = _make_operation(status=OperationStatus.FAILED)

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            mock_number_service = MagicMock()
            mock_number_service.assign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                    "--force",
                ],
            )
            assert result.exit_code == 1
            assert "failed" in result.stdout


class TestNumbersUnassignCommand:
    """Tests for numbers unassign command."""

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

    @pytest.mark.unit
    def test_unassign_requires_number_argument(self) -> None:
        """unassign requires number argument."""
        result = runner.invoke(app, ["numbers", "unassign"])
        assert result.exit_code == 2
        output = result.stdout + (result.stderr or "")
        assert (
            "NUMBER" in output or "Missing argument" in output or result.exit_code == 2
        )

    @pytest.mark.unit
    def test_unassign_with_confirmation_prompt(self) -> None:
        """unassign shows confirmation prompt and can be cancelled."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "unassign", "+14255551234"],
                input="n\n",  # Decline confirmation
            )
            assert result.exit_code == 0
            assert "cancelled" in result.stdout

    @pytest.mark.unit
    def test_unassign_with_force_skips_confirmation(self) -> None:
        """unassign --force skips confirmation prompt."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_operation = _make_operation()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.unassign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "unassign", "+14255551234", "--force"],
            )
            assert result.exit_code == 0
            assert "Unassigned" in result.stdout
            # Verify no prompt was shown (no "Proceed" in output)
            assert "Proceed" not in result.stdout

    @pytest.mark.unit
    def test_unassign_on_already_unassigned_number_shows_error(self) -> None:
        """unassign on unassigned number shows validation error."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.UNASSIGNED,
            assignment_target_id=None,
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "unassign", "+14255551234", "--force"],
            )
            # ValidationError exit code
            assert result.exit_code == 5
            assert "not assigned" in result.stdout

    @pytest.mark.unit
    def test_unassign_dry_run_preview_output(self) -> None:
        """unassign --dry-run shows preview without making changes."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "unassign", "+14255551234", "--dry-run"],
            )
            assert result.exit_code == 0
            assert "Dry Run Preview" in result.stdout
            # Phone numbers and IDs may have ANSI escape codes
            assert "14255551234" in result.stdout
            assert "user-" in result.stdout and "123" in result.stdout
            assert "No changes made" in result.stdout

            # Verify unassign_number was not called
            mock_number_service.unassign_number.assert_not_called()

    @pytest.mark.unit
    def test_unassign_no_wait_returns_immediately(self) -> None:
        """unassign --no-wait returns operation ID without waiting."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_operation = _make_operation(status=OperationStatus.NOT_STARTED)

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.unassign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "unassign",
                    "+14255551234",
                    "--force",
                    "--no-wait",
                ],
            )
            assert result.exit_code == 0
            # Operation ID may have ANSI escape codes
            assert "op-" in result.stdout
            assert "123" in result.stdout
            assert "initiated" in result.stdout

    @pytest.mark.unit
    def test_unassign_json_output_format(self) -> None:
        """unassign --json outputs valid JSON structure."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_operation = _make_operation()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.unassign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--json", "numbers", "unassign", "+14255551234"],
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "succeeded"
            assert data["operation_id"] == "op-123"
            assert data["phone_number"] == "+14255551234"
            assert data["number_type"] == "directRouting"
            assert data["previous_assignee"] == "user-123"

    @pytest.mark.unit
    def test_unassign_success_output(self) -> None:
        """unassign success shows appropriate message."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_operation = _make_operation()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.unassign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "unassign", "+14255551234", "--force"],
            )
            assert result.exit_code == 0
            assert "Unassigned" in result.stdout
            # Phone numbers may have ANSI escape codes
            assert "14255551234" in result.stdout

    @pytest.mark.unit
    def test_unassign_failed_operation_exits_with_error(self) -> None:
        """unassign with failed operation exits with error code."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_operation = _make_operation(status=OperationStatus.FAILED)

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.unassign_number = AsyncMock(return_value=mock_operation)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "unassign", "+14255551234", "--force"],
            )
            assert result.exit_code == 1
            assert "failed" in result.stdout

    @pytest.mark.unit
    def test_unassign_number_not_found_shows_error(self) -> None:
        """unassign with unknown number shows not found error."""
        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(
                side_effect=NotFoundError(
                    "Phone number '+19999999999' not found",
                    remediation="Verify the number exists in your inventory.",
                )
            )
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "unassign", "+19999999999", "--force"],
            )
            assert result.exit_code == 4  # NotFoundError exit code
            assert "not found" in result.stdout

    @pytest.mark.unit
    def test_unassign_dry_run_json_output(self) -> None:
        """unassign --dry-run --json outputs valid JSON."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--json", "numbers", "unassign", "+14255551234", "--dry-run"],
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["dry_run"] is True
            assert data["phone_number"] == "+14255551234"
            assert data["number_type"] == "directRouting"
            assert data["current_assignee"] == "user-123"


class TestNumbersUpdateCommand:
    """Tests for numbers update command."""

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

    @pytest.mark.unit
    def test_update_requires_number_argument(self) -> None:
        """update requires number argument."""
        result = runner.invoke(app, ["numbers", "update"])
        assert result.exit_code == 2
        output = result.stdout + (result.stderr or "")
        assert (
            "NUMBER" in output or "Missing argument" in output or result.exit_code == 2
        )

    @pytest.mark.unit
    def test_update_requires_at_least_one_option(self) -> None:
        """update requires at least one update option."""
        result = runner.invoke(
            app,
            ["numbers", "update", "+14255551234"],
        )
        assert result.exit_code == 5  # ValidationError exit code
        assert "No update parameters specified" in result.stdout

    @pytest.mark.unit
    def test_update_location_and_clear_location_mutually_exclusive(self) -> None:
        """update errors when both --location and --clear-location specified."""
        result = runner.invoke(
            app,
            [
                "numbers",
                "update",
                "+14255551234",
                "--location",
                "Seattle HQ",
                "--clear-location",
            ],
        )
        assert result.exit_code == 5
        assert "Cannot specify both --location and --clear-location" in result.stdout

    @pytest.mark.unit
    def test_update_network_site_and_clear_mutually_exclusive(self) -> None:
        """update errors when both --network-site and --clear-network-site specified."""
        result = runner.invoke(
            app,
            [
                "numbers",
                "update",
                "+14255551234",
                "--network-site",
                "Site-001",
                "--clear-network-site",
            ],
        )
        assert result.exit_code == 5
        assert (
            "Cannot specify both --network-site and --clear-network-site"
            in result.stdout
        )

    @pytest.mark.unit
    def test_update_on_unassigned_number_shows_error(self) -> None:
        """update on unassigned number shows validation error."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.UNASSIGNED,
            assignment_target_id=None,
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "update",
                    "+14255551234",
                    "--clear-location",
                    "--force",
                ],
            )
            # ValidationError exit code
            assert result.exit_code == 5
            assert "not assigned" in result.stdout

    @pytest.mark.unit
    def test_update_dry_run_with_location(self) -> None:
        """update --dry-run shows preview with location."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_location = _make_location()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "update",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                    "--dry-run",
                ],
            )
            assert result.exit_code == 0
            assert "Dry Run Preview" in result.stdout
            assert "14255551234" in result.stdout
            assert "Seattle" in result.stdout
            assert "No changes made" in result.stdout

            # Verify update_number was not called
            mock_number_service.update_number.assert_not_called()

    @pytest.mark.unit
    def test_update_dry_run_with_clear_location(self) -> None:
        """update --dry-run shows preview with clear location."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "update",
                    "+14255551234",
                    "--clear-location",
                    "--dry-run",
                ],
            )
            assert result.exit_code == 0
            assert "Dry Run Preview" in result.stdout
            assert "Will be cleared" in result.stdout

    @pytest.mark.unit
    def test_update_with_confirmation_prompt(self) -> None:
        """update shows confirmation prompt and can be cancelled."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "update", "+14255551234", "--clear-location"],
                input="n\n",  # Decline confirmation
            )
            assert result.exit_code == 0
            assert "cancelled" in result.stdout

    @pytest.mark.unit
    def test_update_with_force_skips_confirmation(self) -> None:
        """update --force skips confirmation prompt."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.update_number = AsyncMock(return_value=None)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "update",
                    "+14255551234",
                    "--clear-location",
                    "--force",
                ],
            )
            assert result.exit_code == 0
            assert "Updated" in result.stdout
            # Verify no prompt was shown
            assert "Proceed" not in result.stdout

    @pytest.mark.unit
    def test_update_json_output_format(self) -> None:
        """update --json outputs valid JSON structure."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.update_number = AsyncMock(return_value=None)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--json", "numbers", "update", "+14255551234", "--clear-location"],
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "success"
            assert data["phone_number"] == "+14255551234"
            assert data["number_type"] == "directRouting"
            assert data["clear_location"] is True

    @pytest.mark.unit
    def test_update_json_dry_run_output(self) -> None:
        """update --json --dry-run outputs preview JSON."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--json",
                    "numbers",
                    "update",
                    "+14255551234",
                    "--network-site",
                    "Site-001",
                    "--dry-run",
                ],
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["dry_run"] is True
            assert data["phone_number"] == "+14255551234"
            assert data["network_site_id"] == "Site-001"

    @pytest.mark.unit
    def test_update_calls_service_with_correct_parameters(self) -> None:
        """update passes correct parameters to NumberService."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_location = _make_location()

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.update_number = AsyncMock(return_value=None)
            mock_ns_class.return_value = mock_number_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "update",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                    "--force",
                ],
            )
            assert result.exit_code == 0

            # Verify update_number was called with correct arguments
            mock_number_service.update_number.assert_called_once()
            call_kwargs = mock_number_service.update_number.call_args[1]
            assert call_kwargs["phone_number"] == "+14255551234"
            assert call_kwargs["location_id"] == "loc-123"
            assert call_kwargs["clear_location"] is False


class TestProgressDisplay:
    """Tests for progress spinner display during assign/unassign operations."""

    @pytest.mark.unit
    def test_assign_wait_calls_on_progress_callback(self) -> None:
        """assign (default wait=True) passes on_progress callback to service."""
        mock_user = _make_user()
        mock_location = _make_location()
        mock_operation = _make_operation()
        captured_on_progress = []

        async def capture_on_progress(
            user_id: str,
            phone_number: str,
            number_type: NumberType,
            *,
            location_id: str | None = None,
            wait: bool = True,
            on_progress: object = None,
            timeout: int = 60,
        ) -> AsyncOperation:
            captured_on_progress.append(on_progress)
            return mock_operation

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            mock_number_service = MagicMock()
            mock_number_service.assign_number = AsyncMock(
                side_effect=capture_on_progress
            )
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                    "--force",
                ],
            )
            assert result.exit_code == 0
            # Verify on_progress callback was passed (not None)
            assert len(captured_on_progress) == 1
            assert captured_on_progress[0] is not None

    @pytest.mark.unit
    def test_assign_json_mode_no_progress_callback(self) -> None:
        """assign --json does not use progress callback (silent mode)."""
        mock_user = _make_user()
        mock_location = _make_location()
        mock_operation = _make_operation()
        captured_on_progress = []

        async def capture_on_progress(
            user_id: str,
            phone_number: str,
            number_type: NumberType,
            *,
            location_id: str | None = None,
            wait: bool = True,
            on_progress: object = None,
            timeout: int = 60,
        ) -> AsyncOperation:
            captured_on_progress.append(on_progress)
            return mock_operation

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_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_user_service = MagicMock()
            mock_user_service.resolve_user = AsyncMock(return_value=mock_user)
            mock_us_class.return_value = mock_user_service

            mock_location_service = MagicMock()
            mock_location_service.validate_location = MagicMock(
                return_value=mock_location
            )
            mock_ls_class.return_value = mock_location_service

            mock_number_service = MagicMock()
            mock_number_service.assign_number = AsyncMock(
                side_effect=capture_on_progress
            )
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                [
                    "--json",
                    "numbers",
                    "assign",
                    "john@contoso.com",
                    "+14255551234",
                    "--location",
                    "Seattle HQ",
                ],
            )
            assert result.exit_code == 0
            # In JSON mode, on_progress should be None (no spinner)
            assert len(captured_on_progress) == 1
            assert captured_on_progress[0] is None

    @pytest.mark.unit
    def test_unassign_wait_calls_on_progress_callback(self) -> None:
        """unassign (default wait=True) passes on_progress callback to service."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_operation = _make_operation()
        captured_on_progress = []

        async def capture_on_progress(
            phone_number: str,
            number_type: NumberType,
            *,
            wait: bool = True,
            on_progress: object = None,
            timeout: int = 60,
        ) -> AsyncOperation:
            captured_on_progress.append(on_progress)
            return mock_operation

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.unassign_number = AsyncMock(
                side_effect=capture_on_progress
            )
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "unassign", "+14255551234", "--force"],
            )
            assert result.exit_code == 0
            # Verify on_progress callback was passed (not None)
            assert len(captured_on_progress) == 1
            assert captured_on_progress[0] is not None

    @pytest.mark.unit
    def test_unassign_json_mode_no_progress_callback(self) -> None:
        """unassign --json does not use progress callback (silent mode)."""
        mock_number = _make_number(
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-123",
        )
        mock_operation = _make_operation()
        captured_on_progress = []

        async def capture_on_progress(
            phone_number: str,
            number_type: NumberType,
            *,
            wait: bool = True,
            on_progress: object = None,
            timeout: int = 60,
        ) -> AsyncOperation:
            captured_on_progress.append(on_progress)
            return mock_operation

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_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_number_service = MagicMock()
            mock_number_service.get_number = AsyncMock(return_value=mock_number)
            mock_number_service.unassign_number = AsyncMock(
                side_effect=capture_on_progress
            )
            mock_ns_class.return_value = mock_number_service

            result = runner.invoke(
                app,
                ["--json", "numbers", "unassign", "+14255551234"],
            )
            assert result.exit_code == 0
            # In JSON mode, on_progress should be None (no spinner)
            assert len(captured_on_progress) == 1
            assert captured_on_progress[0] is None


class TestNumbersBulkAssignCommand:
    """Tests for numbers bulk-assign command."""

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

    @pytest.mark.unit
    def test_bulk_assign_requires_csv_file_argument(self) -> None:
        """bulk-assign requires csv_file argument."""
        result = runner.invoke(app, ["numbers", "bulk-assign"])
        assert result.exit_code == 2
        output = result.stdout + (result.stderr or "")
        assert (
            "CSV_FILE" in output
            or "Missing argument" in output
            or result.exit_code == 2
        )

    @pytest.mark.unit
    def test_bulk_assign_file_not_found(self) -> None:
        """bulk-assign shows error for non-existent file."""
        result = runner.invoke(
            app, ["--no-color", "numbers", "bulk-assign", "nonexistent.csv"]
        )
        assert result.exit_code == 2
        # Typer handles this before our code - file doesn't exist
        output = result.stdout + (result.stderr or "")
        assert "not exist" in output or "not found" in output or result.exit_code == 2

    @pytest.mark.unit
    def test_bulk_assign_dry_run_valid_csv(self, tmp_path: "Path") -> None:
        """bulk-assign --dry-run validates CSV and shows summary."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result
        mock_validation = MagicMock()
        mock_validation.rows = [MagicMock()]
        mock_validation.rows[0].row_number = 2
        mock_validation.rows[0].user = "john@contoso.com"
        mock_validation.rows[0].phone_number = "+14255551234"
        mock_validation.rows[0].location = "Seattle HQ"
        mock_validation.rows[0].errors = []
        mock_validation.rows[0].warnings = []
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService") as mock_us_class,
            patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
            patch("teams_phone.cli.numbers.LocationService") as mock_ls_class,
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_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_user_service = MagicMock()
            mock_us_class.return_value = mock_user_service

            mock_number_service = MagicMock()
            mock_ns_class.return_value = mock_number_service

            mock_location_service = MagicMock()
            mock_ls_class.return_value = mock_location_service

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--dry-run"],
            )
            assert result.exit_code == 0
            assert "Validation Summary" in result.stdout
            # Check for content that may have ANSI codes around numbers
            assert "Total rows:" in result.stdout
            assert "Valid:" in result.stdout
            assert "Errors:" in result.stdout
            assert "Dry run complete" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_dry_run_with_errors(self, tmp_path: "Path") -> None:
        """bulk-assign --dry-run exits with code 1 when validation errors exist."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result with errors
        mock_validation = MagicMock()
        mock_validation.rows = [MagicMock()]
        mock_validation.rows[0].row_number = 2
        mock_validation.rows[0].user = "john@contoso.com"
        mock_validation.rows[0].phone_number = "+14255551234"
        mock_validation.rows[0].location = "Seattle HQ"
        mock_validation.rows[0].errors = ["User 'john@contoso.com' not found"]
        mock_validation.rows[0].warnings = []
        mock_validation.valid_count = 0
        mock_validation.error_count = 1
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_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_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--dry-run"],
            )
            assert result.exit_code == 1  # Exit code 1 for validation errors
            assert "Validation Errors" in result.stdout
            assert "john@contoso.com" in result.stdout
            assert "not found" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_json_dry_run(self, tmp_path: "Path") -> None:
        """bulk-assign --dry-run --json outputs valid JSON."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result
        mock_validation = MagicMock()
        mock_validation.rows = [MagicMock()]
        mock_validation.rows[0].row_number = 2
        mock_validation.rows[0].user = "john@contoso.com"
        mock_validation.rows[0].phone_number = "+14255551234"
        mock_validation.rows[0].location = "Seattle HQ"
        mock_validation.rows[0].errors = []
        mock_validation.rows[0].warnings = []
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_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_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            result = runner.invoke(
                app,
                ["--json", "numbers", "bulk-assign", str(csv_file), "--dry-run"],
            )
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["dry_run"] is True
            assert data["total_rows"] == 1
            assert data["valid_count"] == 1
            assert data["error_count"] == 0
            assert len(data["rows"]) == 1
            assert data["rows"][0]["user"] == "john@contoso.com"
            assert data["rows"][0]["valid"] is True

    @pytest.mark.unit
    def test_bulk_assign_validation_error_without_continue(
        self, tmp_path: "Path"
    ) -> None:
        """bulk-assign fails with ValidationError when errors exist and no --continue-on-error."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result with errors
        mock_validation = MagicMock()
        mock_validation.rows = [MagicMock()]
        mock_validation.rows[0].row_number = 2
        mock_validation.rows[0].user = "john@contoso.com"
        mock_validation.rows[0].phone_number = "+14255551234"
        mock_validation.rows[0].location = "Seattle HQ"
        mock_validation.rows[0].errors = ["User not found"]
        mock_validation.rows[0].warnings = []
        mock_validation.valid_count = 0
        mock_validation.error_count = 1
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_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_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file)],
            )
            # Should exit with ValidationError exit code (5)
            assert result.exit_code == 5
            assert "Validation failed" in result.stdout
            assert "continue-on-error" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_empty_csv_no_rows(self, tmp_path: "Path") -> None:
        """bulk-assign handles CSV with only header (no data rows)."""
        csv_content = "user,phone_number,location\n"
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient"),
        ):
            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--dry-run"],
            )
            assert result.exit_code == 0
            assert "No data rows found" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_default_location_applied(self, tmp_path: "Path") -> None:
        """bulk-assign --default-location applies to rows without location."""
        # CSV without location column
        csv_content = "user,phone_number\njohn@contoso.com,+14255551234\n"
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        captured_rows: list[Any] = []

        async def capture_validation(rows: list[Any]) -> MagicMock:
            captured_rows.extend(rows)
            mock_result = MagicMock()
            mock_result.rows = rows
            mock_result.valid_count = 1
            mock_result.error_count = 0
            mock_result.warning_count = 0
            return mock_result

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_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_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(side_effect=capture_validation)
            mock_bulk_class.return_value = mock_bulk

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "bulk-assign",
                    str(csv_file),
                    "--default-location",
                    "Seattle-HQ",
                    "--dry-run",
                ],
            )
            assert result.exit_code == 0
            # Check that the default location was applied
            assert len(captured_rows) == 1
            assert captured_rows[0].location == "Seattle-HQ"

    @pytest.mark.unit
    def test_bulk_assign_confirmation_cancelled(self, tmp_path: "Path") -> None:
        """bulk-assign exits when user cancels confirmation."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - all valid
        mock_validation = MagicMock()
        mock_validation.rows = [MagicMock()]
        mock_validation.rows[0].row_number = 2
        mock_validation.rows[0].user = "john@contoso.com"
        mock_validation.rows[0].phone_number = "+14255551234"
        mock_validation.rows[0].location = "Seattle HQ"
        mock_validation.rows[0].errors = []
        mock_validation.rows[0].warnings = []
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_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_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Simulate user typing "n" to cancel
            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file)],
                input="n\n",
            )
            assert result.exit_code == 0
            assert "cancelled" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_yes_skips_confirmation(self, tmp_path: "Path") -> None:
        """bulk-assign --yes skips confirmation prompt."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - all valid
        mock_validation = MagicMock()
        mock_validation.rows = [MagicMock()]
        mock_validation.rows[0].row_number = 2
        mock_validation.rows[0].user = "john@contoso.com"
        mock_validation.rows[0].phone_number = "+14255551234"
        mock_validation.rows[0].location = "Seattle HQ"
        mock_validation.rows[0].errors = []
        mock_validation.rows[0].warnings = []
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock successful assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.SUCCEEDED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--yes"],
            )
            # Should complete successfully
            assert result.exit_code == 0
            # Confirmation prompt should not appear
            assert "Proceed with bulk assignment?" not in result.stdout
            # Should show execution summary
            assert "Execution Summary:" in result.stdout
            assert "Successful:" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_continue_on_error_proceeds(self, tmp_path: "Path") -> None:
        """bulk-assign --continue-on-error proceeds with valid rows despite errors."""
        csv_content = (
            "user,phone_number,location\n"
            "john@contoso.com,+14255551234,Seattle HQ\n"
            "invalid@contoso.com,+14255555678,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - one valid, one error
        mock_row1 = MagicMock()
        mock_row1.row_number = 2
        mock_row1.user = "john@contoso.com"
        mock_row1.phone_number = "+14255551234"
        mock_row1.location = "Seattle HQ"
        mock_row1.errors = []
        mock_row1.warnings = []

        mock_row2 = MagicMock()
        mock_row2.row_number = 3
        mock_row2.user = "invalid@contoso.com"
        mock_row2.phone_number = "+14255555678"
        mock_row2.location = "Seattle HQ"
        mock_row2.errors = ["User not found"]
        mock_row2.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row1, mock_row2]
        mock_validation.valid_count = 1
        mock_validation.error_count = 1
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution (only for valid row)
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock successful assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.SUCCEEDED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "bulk-assign",
                    str(csv_file),
                    "--continue-on-error",
                    "--yes",
                ],
            )
            # Should complete execution with mixed results
            assert result.exit_code == 0
            # Validation summary should show errors exist
            assert "Errors:" in result.stdout
            assert "Valid:" in result.stdout
            # Execution summary should appear
            assert "Execution Summary:" in result.stdout
            assert "Successful:" in result.stdout
            assert "Skipped:" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_execution_success_increments_counter(
        self, tmp_path: "Path"
    ) -> None:
        """bulk-assign increments success counter on successful assignment."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - all valid
        mock_row = MagicMock()
        mock_row.row_number = 2
        mock_row.user = "john@contoso.com"
        mock_row.phone_number = "+14255551234"
        mock_row.location = "Seattle HQ"
        mock_row.errors = []
        mock_row.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row]
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock successful assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.SUCCEEDED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--yes"],
            )
            assert result.exit_code == 0
            # Check for labels (numbers may have ANSI codes)
            assert "Successful:" in result.stdout
            assert "Failed:" in result.stdout
            # Verify assignment was called
            mock_bulk.number_service.assign_number.assert_called_once()

    @pytest.mark.unit
    def test_bulk_assign_execution_failure_increments_counter(
        self, tmp_path: "Path"
    ) -> None:
        """bulk-assign increments fail counter on failed assignment."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - all valid
        mock_row = MagicMock()
        mock_row.row_number = 2
        mock_row.user = "john@contoso.com"
        mock_row.phone_number = "+14255551234"
        mock_row.location = "Seattle HQ"
        mock_row.errors = []
        mock_row.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row]
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock FAILED assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.FAILED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--yes"],
            )
            # Exit code 1 when there are failures
            assert result.exit_code == 1
            assert "Successful:" in result.stdout
            assert "Failed:" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_execution_exception_increments_fail_counter(
        self, tmp_path: "Path"
    ) -> None:
        """bulk-assign increments fail counter when assignment throws exception."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - all valid
        mock_row = MagicMock()
        mock_row.row_number = 2
        mock_row.user = "john@contoso.com"
        mock_row.phone_number = "+14255551234"
        mock_row.location = "Seattle HQ"
        mock_row.errors = []
        mock_row.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row]
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution that throws exception
            mock_bulk.user_service.resolve_user = AsyncMock(
                side_effect=NotFoundError("User not found")
            )

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--yes"],
            )
            # Exit code 1 when there are failures
            assert result.exit_code == 1
            assert "Successful:" in result.stdout
            assert "Failed:" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_json_mode_suppresses_progress(self, tmp_path: "Path") -> None:
        """bulk-assign --json suppresses progress bar display."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - all valid
        mock_row = MagicMock()
        mock_row.row_number = 2
        mock_row.user = "john@contoso.com"
        mock_row.phone_number = "+14255551234"
        mock_row.location = "Seattle HQ"
        mock_row.errors = []
        mock_row.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row]
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock successful assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.SUCCEEDED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                ["--json", "numbers", "bulk-assign", str(csv_file), "--yes"],
            )
            assert result.exit_code == 0
            # In JSON mode, validation result is output as JSON
            # No progress bar text should appear
            assert "Processing [" not in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_skips_rows_with_validation_errors(
        self, tmp_path: "Path"
    ) -> None:
        """bulk-assign skips rows that failed validation and marks them as skipped."""
        csv_content = (
            "user,phone_number,location\n"
            "john@contoso.com,+14255551234,Seattle HQ\n"
            "invalid@contoso.com,+14255555678,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - one valid, one with error
        mock_row1 = MagicMock()
        mock_row1.row_number = 2
        mock_row1.user = "john@contoso.com"
        mock_row1.phone_number = "+14255551234"
        mock_row1.location = "Seattle HQ"
        mock_row1.errors = []
        mock_row1.warnings = []

        mock_row2 = MagicMock()
        mock_row2.row_number = 3
        mock_row2.user = "invalid@contoso.com"
        mock_row2.phone_number = "+14255555678"
        mock_row2.location = "Seattle HQ"
        mock_row2.errors = ["User not found"]
        mock_row2.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row1, mock_row2]
        mock_validation.valid_count = 1
        mock_validation.error_count = 1
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution (only called for valid row)
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock successful assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.SUCCEEDED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "bulk-assign",
                    str(csv_file),
                    "--continue-on-error",
                    "--yes",
                ],
            )
            assert result.exit_code == 0
            assert "Successful:" in result.stdout
            assert "Skipped:" in result.stdout
            # assign_number should only be called once (for valid row)
            mock_bulk.number_service.assign_number.assert_called_once()

    @pytest.mark.unit
    def test_bulk_assign_default_location_from_tenant_profile(
        self, tmp_path: "Path"
    ) -> None:
        """bulk-assign uses tenant profile default_location when --default-location not set."""
        # CSV without location column
        csv_content = "user,phone_number\njohn@contoso.com,+14255551234\n"
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        captured_rows: list[Any] = []

        async def capture_validation(rows: list[Any]) -> MagicMock:
            captured_rows.extend(rows)
            mock_result = MagicMock()
            mock_result.rows = rows
            mock_result.valid_count = 1
            mock_result.error_count = 0
            mock_result.warning_count = 0
            return mock_result

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService") as mock_loc_class,
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.infrastructure.ConfigManager") as mock_config_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 config manager to return tenant with default_location
            mock_config = MagicMock()
            mock_config.get_default_tenant.return_value = "test-tenant"
            mock_tenant = MagicMock()
            mock_tenant.default_location = "Tenant-Default-Location"
            mock_config.get_tenant.return_value = mock_tenant
            mock_config_class.return_value = mock_config

            # Mock location validation (called for default location)
            mock_location = MagicMock()
            mock_location.location_id = "loc-tenant-default"
            mock_loc_class.return_value.validate_location.return_value = mock_location

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(side_effect=capture_validation)
            mock_bulk_class.return_value = mock_bulk

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "bulk-assign",
                    str(csv_file),
                    "--dry-run",
                ],
            )
            assert result.exit_code == 0
            # Check that the tenant default location was applied
            assert len(captured_rows) == 1
            assert captured_rows[0].location == "Tenant-Default-Location"
            # Should show info message about using tenant default
            assert "Using default location from tenant profile" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_stops_on_first_error_without_continue(
        self, tmp_path: "Path"
    ) -> None:
        """bulk-assign stops processing on first error when --continue-on-error not set."""
        csv_content = (
            "user,phone_number,location\n"
            "john@contoso.com,+14255551234,Seattle HQ\n"
            "jane@contoso.com,+14255555678,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result - both valid
        mock_row1 = MagicMock()
        mock_row1.row_number = 2
        mock_row1.user = "john@contoso.com"
        mock_row1.phone_number = "+14255551234"
        mock_row1.location = "Seattle HQ"
        mock_row1.errors = []
        mock_row1.warnings = []

        mock_row2 = MagicMock()
        mock_row2.row_number = 3
        mock_row2.user = "jane@contoso.com"
        mock_row2.phone_number = "+14255555678"
        mock_row2.location = "Seattle HQ"
        mock_row2.errors = []
        mock_row2.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row1, mock_row2]
        mock_validation.valid_count = 2
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution that fails on first call
            mock_bulk.user_service.resolve_user = AsyncMock(
                side_effect=NotFoundError("User not found")
            )

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--yes"],
            )
            # Exit code 1 when there are failures
            assert result.exit_code == 1
            # Should only call resolve_user once (stopped after first error)
            assert mock_bulk.user_service.resolve_user.call_count == 1
            # Should show skipped count for second row
            assert "Skipped:" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_writes_results_file(self, tmp_path: "Path") -> None:
        """bulk-assign writes results to specified output file."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)
        output_file = tmp_path / "results.csv"

        # Create mock validation result
        mock_row = MagicMock()
        mock_row.row_number = 2
        mock_row.user = "john@contoso.com"
        mock_row.phone_number = "+14255551234"
        mock_row.location = "Seattle HQ"
        mock_row.errors = []
        mock_row.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row]
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv") as mock_write,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock successful assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.SUCCEEDED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                [
                    "--no-color",
                    "numbers",
                    "bulk-assign",
                    str(csv_file),
                    "--yes",
                    "--output",
                    str(output_file),
                ],
            )
            assert result.exit_code == 0
            # Verify write_results_csv was called with correct output path
            mock_write.assert_called_once()
            call_args = mock_write.call_args
            assert call_args[0][1] == output_file
            # Should show results file in output
            assert "Results file:" in result.stdout

    @pytest.mark.unit
    def test_bulk_assign_json_output_includes_results(self, tmp_path: "Path") -> None:
        """bulk-assign --json includes results array in output."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result
        mock_row = MagicMock()
        mock_row.row_number = 2
        mock_row.user = "john@contoso.com"
        mock_row.phone_number = "+14255551234"
        mock_row.location = "Seattle HQ"
        mock_row.errors = []
        mock_row.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row]
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv"),
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock successful assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.SUCCEEDED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                ["--json", "numbers", "bulk-assign", str(csv_file), "--yes"],
            )
            assert result.exit_code == 0
            # Parse JSON output
            output = json.loads(result.stdout)
            assert output["status"] == "completed"
            assert "summary" in output
            assert output["summary"]["successful"] == 1
            assert output["summary"]["failed"] == 0
            assert "results" in output
            assert len(output["results"]) == 1
            assert output["results"][0]["status"] == "success"
            assert output["results"][0]["user"] == "john@contoso.com"

    @pytest.mark.unit
    def test_bulk_assign_generates_timestamp_filename(self, tmp_path: "Path") -> None:
        """bulk-assign generates timestamp-based filename when --output not specified."""
        csv_content = (
            "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
        )
        csv_file = tmp_path / "test.csv"
        csv_file.write_text(csv_content)

        # Create mock validation result
        mock_row = MagicMock()
        mock_row.row_number = 2
        mock_row.user = "john@contoso.com"
        mock_row.phone_number = "+14255551234"
        mock_row.location = "Seattle HQ"
        mock_row.errors = []
        mock_row.warnings = []

        mock_validation = MagicMock()
        mock_validation.rows = [mock_row]
        mock_validation.valid_count = 1
        mock_validation.error_count = 0
        mock_validation.warning_count = 0

        with (
            patch("teams_phone.cli.numbers.CacheManager"),
            patch("teams_phone.cli.numbers.AuthService"),
            patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
            patch("teams_phone.cli.numbers.UserService"),
            patch("teams_phone.cli.numbers.NumberService"),
            patch("teams_phone.cli.numbers.LocationService"),
            patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
            patch("teams_phone.cli.numbers.write_results_csv") as mock_write,
            patch("teams_phone.cli.numbers.datetime") as mock_datetime,
        ):
            mock_gc = AsyncMock()
            mock_gc.__aenter__.return_value = mock_gc
            mock_gc.__aexit__.return_value = None
            mock_gc_class.return_value = mock_gc

            # Mock datetime to control timestamp
            mock_now = MagicMock()
            mock_now.strftime.return_value = "20260129-120000"
            mock_datetime.now.return_value = mock_now

            mock_bulk = MagicMock()
            mock_bulk.validate_assign_csv = AsyncMock(return_value=mock_validation)
            mock_bulk_class.return_value = mock_bulk

            # Mock user resolution
            mock_user = MagicMock()
            mock_user.id = "user-123"
            mock_bulk.user_service.resolve_user = AsyncMock(return_value=mock_user)

            # Mock number lookup
            mock_number = MagicMock()
            mock_number.number_type = NumberType.DIRECT_ROUTING
            mock_bulk.number_service.get_number = AsyncMock(return_value=mock_number)

            # Mock successful assignment
            mock_operation = MagicMock()
            mock_operation.status = OperationStatus.SUCCEEDED
            mock_bulk.number_service.assign_number = AsyncMock(
                return_value=mock_operation
            )

            # Mock location validation
            mock_location = MagicMock()
            mock_location.location_id = "loc-123"
            mock_bulk.location_service.validate_location = MagicMock(
                return_value=mock_location
            )

            result = runner.invoke(
                app,
                ["--no-color", "numbers", "bulk-assign", str(csv_file), "--yes"],
            )
            assert result.exit_code == 0
            # Verify write_results_csv was called with timestamp-based filename
            mock_write.assert_called_once()
            call_args = mock_write.call_args
            output_path = call_args[0][1]
            assert "bulk-assign-results-20260129-120000.csv" in str(output_path)
