"""Integration tests for CLI numbers commands.

This module tests numbers command workflows including list/show/search/assign/unassign
operations, filter options, and output format validation (table/JSON).
"""

from __future__ import annotations

import json
from datetime import datetime, timezone
from typing import Generator
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from typer.testing import CliRunner

from teams_phone.cli.main import app
from teams_phone.exceptions import NotFoundError
from teams_phone.models import (
    AssignmentStatus,
    AsyncOperation,
    NumberType,
    OperationStatus,
)

from tests.integration.cli.conftest import (
    make_location,
    make_number,
    make_user_config,
)


class TestNumbersListIntegration:
    """Integration tests for numbers list command."""

    @pytest.mark.integration
    def test_list_all_numbers_table_output(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """List without filters returns all numbers in table format."""
        mocks = mock_cli_services_numbers
        numbers = [
            make_number(telephone_number="+14255551234", city="Seattle"),
            make_number(
                number_id="num-22222222-2222-2222-2222-222222222222",
                telephone_number="+14255555678",
                city="Portland",
            ),
        ]
        mocks["number_service"].list_numbers.return_value = numbers

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

    @pytest.mark.integration
    def test_list_numbers_json_output(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """List numbers with --json produces valid JSON array."""
        mocks = mock_cli_services_numbers
        numbers = [
            make_number(
                number_id="num-12345",
                telephone_number="+14255551234",
                city="Seattle",
                number_type=NumberType.CALLING_PLAN,
            ),
        ]
        mocks["number_service"].list_numbers.return_value = numbers

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

        data = json.loads(result.stdout)
        assert isinstance(data, list)
        assert len(data) == 1
        assert data[0]["telephone_number"] == "+14255551234"
        assert data[0]["id"] == "num-12345"
        assert data[0]["city"] == "Seattle"
        assert data[0]["number_type"] == "callingPlan"

    @pytest.mark.integration
    def test_list_numbers_status_filter(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--status filter passes correct parameter to service."""
        mocks = mock_cli_services_numbers
        numbers = [
            make_number(
                telephone_number="+14255551234",
                assignment_status=AssignmentStatus.UNASSIGNED,
            ),
        ]
        mocks["number_service"].list_numbers.return_value = numbers

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

        # Verify the service was called with correct status filter
        call_kwargs = mocks["number_service"].list_numbers.call_args[1]
        assert call_kwargs["status"] == AssignmentStatus.UNASSIGNED

    @pytest.mark.integration
    def test_list_numbers_type_filter(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--type filter passes correct parameter to service."""
        mocks = mock_cli_services_numbers
        numbers = [
            make_number(
                telephone_number="+14255551234",
                number_type=NumberType.DIRECT_ROUTING,
            ),
        ]
        mocks["number_service"].list_numbers.return_value = numbers

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

        call_kwargs = mocks["number_service"].list_numbers.call_args[1]
        assert call_kwargs["number_type"] == NumberType.DIRECT_ROUTING

    @pytest.mark.integration
    def test_list_numbers_city_filter(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--city filter passes correct parameter to service."""
        mocks = mock_cli_services_numbers
        numbers = [
            make_number(telephone_number="+14255551234", city="Seattle"),
        ]
        mocks["number_service"].list_numbers.return_value = numbers

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

        call_kwargs = mocks["number_service"].list_numbers.call_args[1]
        assert call_kwargs["city"] == "Seattle"

    @pytest.mark.integration
    def test_list_numbers_limit_option(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--limit option passes correct max_items to service."""
        mocks = mock_cli_services_numbers
        numbers = [make_number(telephone_number="+14255551234")]
        mocks["number_service"].list_numbers.return_value = numbers

        result = runner.invoke(app, ["--no-color", "numbers", "list", "--limit", "10"])
        assert result.exit_code == 0

        call_kwargs = mocks["number_service"].list_numbers.call_args[1]
        assert call_kwargs["max_items"] == 10

    @pytest.mark.integration
    def test_list_numbers_all_option(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--all option passes max_items=None to service."""
        mocks = mock_cli_services_numbers
        numbers = [make_number(telephone_number="+14255551234")]
        mocks["number_service"].list_numbers.return_value = numbers

        result = runner.invoke(app, ["--no-color", "numbers", "list", "--all"])
        assert result.exit_code == 0

        call_kwargs = mocks["number_service"].list_numbers.call_args[1]
        assert call_kwargs["max_items"] is None

    @pytest.mark.integration
    def test_list_numbers_limit_and_all_mutually_exclusive(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--limit and --all together produces ValidationError exit code 5."""
        result = runner.invoke(
            app, ["--no-color", "numbers", "list", "--limit", "10", "--all"]
        )
        assert result.exit_code == 5
        assert "Cannot specify both" in result.stdout

    @pytest.mark.integration
    def test_list_numbers_empty_result(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Empty result shows info message in table mode."""
        mocks = mock_cli_services_numbers
        mocks["number_service"].list_numbers.return_value = []

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


class TestNumbersShowIntegration:
    """Integration tests for numbers show command."""

    @pytest.mark.integration
    def test_show_number_table_output(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Show number displays detailed information."""
        mocks = mock_cli_services_numbers
        number = make_number(
            number_id="num-12345",
            telephone_number="+14255551234",
            number_type=NumberType.CALLING_PLAN,
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            city="Seattle",
        )
        mocks["number_service"].get_number.return_value = number

        result = runner.invoke(app, ["--no-color", "numbers", "show", "+14255551234"])
        assert result.exit_code == 0
        # Phone number may have ANSI highlight codes around digits
        assert "14255551234" in result.stdout
        assert "12345" in result.stdout
        assert "Seattle" in result.stdout
        assert "Calling Plan" in result.stdout

    @pytest.mark.integration
    def test_show_number_json_output(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Show number with --json produces valid JSON object."""
        mocks = mock_cli_services_numbers
        number = make_number(
            number_id="num-12345",
            telephone_number="+14255551234",
            city="Seattle",
        )
        mocks["number_service"].get_number.return_value = number

        result = runner.invoke(app, ["--json", "numbers", "show", "+14255551234"])
        assert result.exit_code == 0

        data = json.loads(result.stdout)
        assert data["id"] == "num-12345"
        assert data["telephone_number"] == "+14255551234"
        assert data["city"] == "Seattle"

    @pytest.mark.integration
    def test_show_number_not_found(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """NotFoundError results in exit code 4."""
        mocks = mock_cli_services_numbers
        mocks["number_service"].get_number.side_effect = NotFoundError(
            "Phone number '+19999999999' not found",
            remediation="Verify the phone number is in your inventory.",
        )

        result = runner.invoke(app, ["numbers", "show", "+19999999999"])
        assert result.exit_code == 4
        assert "not found" in result.stdout.lower()


class TestNumbersSearchIntegration:
    """Integration tests for numbers search command."""

    @pytest.mark.integration
    def test_search_numbers_table_output(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Search returns matching numbers in table format."""
        mocks = mock_cli_services_numbers
        numbers = [
            make_number(telephone_number="+14255551234", city="Seattle"),
            make_number(
                number_id="num-22222222-2222-2222-2222-222222222222",
                telephone_number="+14255555678",
                city="Seattle",
            ),
        ]
        mocks["number_service"].search_numbers.return_value = numbers

        result = runner.invoke(app, ["--no-color", "numbers", "search", "+1425*"])
        assert result.exit_code == 0
        assert "+14255551234" in result.stdout
        assert "+14255555678" in result.stdout
        assert "Search Results" in result.stdout

    @pytest.mark.integration
    def test_search_numbers_json_output(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Search with --json produces valid JSON array."""
        mocks = mock_cli_services_numbers
        numbers = [
            make_number(telephone_number="+14255551234"),
        ]
        mocks["number_service"].search_numbers.return_value = numbers

        result = runner.invoke(app, ["--json", "numbers", "search", "*1234"])
        assert result.exit_code == 0

        data = json.loads(result.stdout)
        assert isinstance(data, list)
        assert len(data) == 1
        assert data[0]["telephone_number"] == "+14255551234"

    @pytest.mark.integration
    def test_search_numbers_with_limit(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--limit option passes max_results to service."""
        mocks = mock_cli_services_numbers
        numbers = [make_number(telephone_number="+14255551234")]
        mocks["number_service"].search_numbers.return_value = numbers

        result = runner.invoke(
            app, ["--no-color", "numbers", "search", "*1234", "--limit", "5"]
        )
        assert result.exit_code == 0

        call_kwargs = mocks["number_service"].search_numbers.call_args[1]
        assert call_kwargs["max_results"] == 5

    @pytest.mark.integration
    def test_search_numbers_empty_result(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Empty search result shows info message."""
        mocks = mock_cli_services_numbers
        mocks["number_service"].search_numbers.return_value = []

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


class TestNumbersAssignIntegration:
    """Integration tests for numbers assign command."""

    @pytest.fixture
    def mock_cli_services_numbers_assign(
        self,
        mock_cache_manager: MagicMock,
        mock_auth_service: MagicMock,
        mock_number_service: MagicMock,
        mock_user_service: MagicMock,
        mock_location_service: MagicMock,
    ) -> Generator[dict[str, MagicMock], None, None]:
        """Patch all services needed for numbers assign command."""
        mock_gc = AsyncMock()
        mock_gc.__aenter__.return_value = mock_gc
        mock_gc.__aexit__.return_value = None

        mock_config_manager = MagicMock()
        mock_config_manager.get_default_tenant.return_value = None

        with (
            patch(
                "teams_phone.cli.numbers.CacheManager", return_value=mock_cache_manager
            ),
            patch(
                "teams_phone.cli.numbers.AuthService", return_value=mock_auth_service
            ),
            patch("teams_phone.cli.numbers.GraphClient", return_value=mock_gc),
            patch(
                "teams_phone.cli.numbers.NumberService",
                return_value=mock_number_service,
            ),
            patch(
                "teams_phone.cli.numbers.UserService", return_value=mock_user_service
            ),
            patch(
                "teams_phone.cli.numbers.LocationService",
                return_value=mock_location_service,
            ),
        ):
            yield {
                "cache_manager": mock_cache_manager,
                "auth_service": mock_auth_service,
                "graph_client": mock_gc,
                "number_service": mock_number_service,
                "user_service": mock_user_service,
                "location_service": mock_location_service,
            }

    @pytest.mark.integration
    def test_assign_number_success(
        self,
        runner: CliRunner,
        mock_cli_services_numbers_assign: dict[str, MagicMock],
    ) -> None:
        """Successful assignment with --force bypasses confirmation."""
        mocks = mock_cli_services_numbers_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        location = make_location(
            location_id="loc-12345",
            description="Seattle HQ",
        )
        operation = AsyncOperation(
            id="op-123",
            created_date_time=datetime.now(timezone.utc),
            status=OperationStatus.SUCCEEDED,
        )

        mocks["user_service"].resolve_user.return_value = user
        mocks["location_service"].validate_location.return_value = location
        mocks["number_service"].assign_number.return_value = operation

        result = runner.invoke(
            app,
            [
                "--no-color",
                "numbers",
                "assign",
                "john@contoso.com",
                "+14255551234",
                "--location",
                "Seattle HQ",
                "--force",
            ],
        )
        assert result.exit_code == 0
        assert "Assigned" in result.stdout
        # Phone number may have ANSI highlight codes around digits
        assert "14255551234" in result.stdout

    @pytest.mark.integration
    def test_assign_number_dry_run(
        self,
        runner: CliRunner,
        mock_cli_services_numbers_assign: dict[str, MagicMock],
    ) -> None:
        """--dry-run previews without making changes."""
        mocks = mock_cli_services_numbers_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        location = make_location(
            location_id="loc-12345",
            description="Seattle HQ",
        )

        mocks["user_service"].resolve_user.return_value = user
        mocks["location_service"].validate_location.return_value = location

        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" in result.stdout
        assert "No changes made" in result.stdout

        # Verify assign_number was NOT called
        mocks["number_service"].assign_number.assert_not_called()

    @pytest.mark.integration
    def test_assign_number_dry_run_json(
        self,
        runner: CliRunner,
        mock_cli_services_numbers_assign: dict[str, MagicMock],
    ) -> None:
        """--dry-run with --json produces valid JSON preview."""
        mocks = mock_cli_services_numbers_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )
        location = make_location(
            location_id="loc-12345",
            description="Seattle HQ",
        )

        mocks["user_service"].resolve_user.return_value = user
        mocks["location_service"].validate_location.return_value = location

        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-12345"
        assert data["phone_number"] == "+14255551234"
        assert data["location_id"] == "loc-12345"

    @pytest.mark.integration
    def test_assign_number_missing_location_validation_error(
        self,
        runner: CliRunner,
        mock_cli_services_numbers_assign: dict[str, MagicMock],
    ) -> None:
        """Missing location for Calling Plan number results in exit code 5."""
        mocks = mock_cli_services_numbers_assign
        user = make_user_config(
            user_id="user-12345",
            upn="john@contoso.com",
            display_name="John Doe",
        )

        mocks["user_service"].resolve_user.return_value = user
        # No location provided, Calling Plan requires location

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

    @pytest.mark.integration
    def test_assign_number_user_not_found(
        self,
        runner: CliRunner,
        mock_cli_services_numbers_assign: dict[str, MagicMock],
    ) -> None:
        """NotFoundError for user results in exit code 4."""
        mocks = mock_cli_services_numbers_assign
        mocks["user_service"].resolve_user.side_effect = NotFoundError(
            "User 'unknown@contoso.com' not found",
            remediation="Verify the user principal name is correct.",
        )

        result = runner.invoke(
            app,
            [
                "--no-color",
                "numbers",
                "assign",
                "unknown@contoso.com",
                "+14255551234",
                "--location",
                "Seattle HQ",
                "--force",
            ],
        )
        assert result.exit_code == 4
        assert "not found" in result.stdout.lower()


class TestNumbersUnassignIntegration:
    """Integration tests for numbers unassign command."""

    @pytest.mark.integration
    def test_unassign_number_success(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Successful unassignment with --force bypasses confirmation."""
        mocks = mock_cli_services_numbers
        number = make_number(
            telephone_number="+14255551234",
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-12345",
        )
        operation = AsyncOperation(
            id="op-123",
            created_date_time=datetime.now(timezone.utc),
            status=OperationStatus.SUCCEEDED,
        )

        mocks["number_service"].get_number.return_value = number
        mocks["number_service"].unassign_number.return_value = operation

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

    @pytest.mark.integration
    def test_unassign_number_dry_run(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--dry-run previews without making changes."""
        mocks = mock_cli_services_numbers
        number = make_number(
            telephone_number="+14255551234",
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-12345",
        )

        mocks["number_service"].get_number.return_value = number

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

        # Verify unassign_number was NOT called
        mocks["number_service"].unassign_number.assert_not_called()

    @pytest.mark.integration
    def test_unassign_number_dry_run_json(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """--dry-run with --json produces valid JSON preview."""
        mocks = mock_cli_services_numbers
        number = make_number(
            telephone_number="+14255551234",
            number_type=NumberType.CALLING_PLAN,
            assignment_status=AssignmentStatus.USER_ASSIGNED,
            assignment_target_id="user-12345",
        )

        mocks["number_service"].get_number.return_value = number

        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["current_assignee"] == "user-12345"

    @pytest.mark.integration
    def test_unassign_number_not_assigned_validation_error(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """Unassigning an unassigned number results in exit code 5."""
        mocks = mock_cli_services_numbers
        number = make_number(
            telephone_number="+14255551234",
            assignment_status=AssignmentStatus.UNASSIGNED,
        )

        mocks["number_service"].get_number.return_value = number

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

    @pytest.mark.integration
    def test_unassign_number_not_found(
        self,
        runner: CliRunner,
        mock_cli_services_numbers: dict[str, MagicMock],
    ) -> None:
        """NotFoundError results in exit code 4."""
        mocks = mock_cli_services_numbers
        mocks["number_service"].get_number.side_effect = NotFoundError(
            "Phone number '+19999999999' not found",
            remediation="Verify the phone number is in your inventory.",
        )

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