"""Pytest configuration and shared fixtures for CLI integration tests.

This module provides reusable fixtures for testing CLI commands with mocked
services. All fixtures are function-scoped by default to ensure test isolation.

Fixture Categories:
    - Runner: CliRunner for invoking CLI commands
    - Service Mocks: Individual mock fixtures for each service
    - Model Factories: Helper functions to create test data
    - Composite Fixtures: Pre-configured mocking contexts
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Generator
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import UUID

import pytest
from typer.testing import CliRunner

from teams_phone.models import (
    AccountType,
    ActivationState,
    AssignmentStatus,
    AuthMethod,
    AuthStatus,
    AuthStatusType,
    CachedToken,
    EffectivePolicyAssignment,
    EmergencyLocation,
    NumberAssignment,
    NumberType,
    Policy,
    PolicyAssignmentDetail,
    TelephoneNumber,
    TenantProfile,
    UserConfiguration,
)

# =============================================================================
# CliRunner Fixture
# =============================================================================


@pytest.fixture
def runner() -> CliRunner:
    """Create a CliRunner for invoking CLI commands.

    Returns:
        A CliRunner instance configured for testing.

    Example:
        def test_command(runner):
            result = runner.invoke(app, ["users", "list"])
            assert result.exit_code == 0
    """
    return CliRunner()


# =============================================================================
# Model Factory Helpers
# =============================================================================


def make_user_config(
    user_id: str = "11111111-1111-1111-1111-111111111111",
    upn: str = "john.doe@contoso.com",
    display_name: str = "John Doe",
    account_type: AccountType = AccountType.USER,
    is_enterprise_voice_enabled: bool = True,
    phone_number: str | None = "+14255551234",
    policies: list[tuple[str, str]] | None = None,
) -> UserConfiguration:
    """Create a UserConfiguration for testing.

    Args:
        user_id: User ID (UUID format string).
        upn: User principal name (email).
        display_name: Display name of the user.
        account_type: Type of account (user, room, etc.).
        is_enterprise_voice_enabled: Whether enterprise voice is enabled.
        phone_number: Primary phone number (or None if not assigned).
        policies: List of (policy_type, display_name) tuples for policy assignments.

    Returns:
        A UserConfiguration instance with the specified attributes.

    Example:
        user = make_user_config(
            upn="test@contoso.com",
            phone_number=None,
            policies=[("TeamsCallingPolicy", "AllowCalling")]
        )
    """
    telephone_numbers = []
    if phone_number:
        telephone_numbers.append(
            TelephoneNumber(
                telephone_number=phone_number,
                assignment_category="primary",
            )
        )

    effective_policy_assignments = []
    if policies:
        for policy_type, policy_name in policies:
            effective_policy_assignments.append(
                EffectivePolicyAssignment(
                    policy_type=policy_type,
                    policy_assignment=PolicyAssignmentDetail(
                        display_name=policy_name,
                        assignment_type="direct",
                        policy_id=f"policy-{policy_type.lower()}-123",
                        group_id=None,
                    ),
                )
            )
    elif is_enterprise_voice_enabled:
        # Add a default policy if enterprise voice is enabled
        effective_policy_assignments.append(
            EffectivePolicyAssignment(
                policy_type="TeamsCallingPolicy",
                policy_assignment=PolicyAssignmentDetail(
                    display_name="AllowCalling",
                    assignment_type="direct",
                    policy_id="policy-123",
                    group_id=None,
                ),
            )
        )

    return UserConfiguration(
        id=user_id,
        user_principal_name=upn,
        tenant_id="tenant-123",
        display_name=display_name,
        account_type=account_type,
        is_enterprise_voice_enabled=is_enterprise_voice_enabled,
        telephone_numbers=telephone_numbers,
        feature_types=["Teams", "CallingPlan"] if is_enterprise_voice_enabled else [],
        effective_policy_assignments=effective_policy_assignments,
    )


def make_location(
    location_id: str = "loc-11111111-1111-1111-1111-111111111111",
    civic_address_id: str = "civic-22222222-2222-2222-2222-222222222222",
    description: str = "Seattle HQ",
    company_name: str = "Contoso",
    address: str = "123 Main St",
    city: str = "Seattle",
    state_or_province: str = "WA",
    postal_code: str = "98101",
    country_or_region: str = "US",
    is_default: bool = True,
) -> EmergencyLocation:
    """Create an EmergencyLocation for testing.

    Args:
        location_id: Unique location identifier.
        civic_address_id: Associated civic address ID.
        description: Location description/name.
        company_name: Company name at location.
        address: Street address.
        city: City name.
        state_or_province: State/province code.
        postal_code: Postal/ZIP code.
        country_or_region: Country/region code.
        is_default: Whether this is the default location.

    Returns:
        An EmergencyLocation instance with the specified attributes.

    Example:
        location = make_location(
            description="Portland Office",
            city="Portland",
            state_or_province="OR"
        )
    """
    return EmergencyLocation.model_validate(
        {
            "locationId": location_id,
            "civicAddressId": civic_address_id,
            "description": description,
            "companyName": company_name,
            "address": address,
            "city": city,
            "stateOrProvince": state_or_province,
            "postalCode": postal_code,
            "countryOrRegion": country_or_region,
            "isDefault": is_default,
        }
    )


def make_number(
    number_id: str = "num-11111111-1111-1111-1111-111111111111",
    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.

    Args:
        number_id: Unique number identifier.
        telephone_number: Phone number in E.164 format.
        number_type: Type of number (direct routing, calling plan, etc.).
        assignment_status: Current assignment status.
        activation_state: Current activation state.
        city: City associated with the number.
        location_id: Associated location ID (if any).
        assignment_target_id: ID of assigned user/resource (if assigned).

    Returns:
        A NumberAssignment instance with the specified attributes.

    Example:
        number = make_number(
            telephone_number="+14255559999",
            assignment_status=AssignmentStatus.ASSIGNED,
            assignment_target_id="user-123"
        )
    """
    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,
        }
    )


def make_policy(
    policy_type: str = "TeamsCallingPolicy",
    policy_name: str = "AllowCalling",
    policy_id: str = "policy-11111111-1111-1111-1111-111111111111",
    description: str | None = "Allow calling for all users",
    is_global: bool = False,
) -> Policy:
    """Create a Policy for testing.

    Args:
        policy_type: Type of policy (TeamsCallingPolicy, TenantDialPlan, etc.).
        policy_name: Name of the policy.
        policy_id: Unique policy identifier.
        description: Policy description (optional).
        is_global: Whether this is the global (default) policy.

    Returns:
        A Policy instance with the specified attributes.

    Example:
        policy = make_policy(
            policy_type="TenantDialPlan",
            policy_name="US-DialPlan",
            is_global=True
        )
    """
    return Policy.model_validate(
        {
            "policyType": policy_type,
            "policyName": policy_name,
            "policyId": policy_id,
            "description": description,
            "isGlobal": is_global,
        }
    )


def make_tenant(
    name: str = "test-tenant",
    display_name: str = "Test Tenant",
    tenant_id: str = "11111111-1111-1111-1111-111111111111",
    client_id: str = "22222222-2222-2222-2222-222222222222",
    auth_method: AuthMethod = AuthMethod.CERTIFICATE,
    cert_path: str = "/path/to/cert.pem",
) -> TenantProfile:
    """Create a TenantProfile for testing.

    Args:
        name: Short name for the tenant (used in CLI).
        display_name: Human-readable display name.
        tenant_id: Azure AD tenant ID.
        client_id: App registration client ID.
        auth_method: Authentication method (certificate or secret).
        cert_path: Path to certificate file.

    Returns:
        A TenantProfile instance with the specified attributes.

    Example:
        tenant = make_tenant(
            name="contoso",
            display_name="Contoso Ltd"
        )
    """
    return TenantProfile(
        name=name,
        display_name=display_name,
        tenant_id=UUID(tenant_id),
        client_id=UUID(client_id),
        auth_method=auth_method,
        cert_path=cert_path,
    )


def make_auth_status(
    status: AuthStatusType = AuthStatusType.AUTHENTICATED,
    tenant_name: str = "test-tenant",
    tenant_id: str | None = "12345-tenant-id",
    expires_at: datetime | None = None,
) -> AuthStatus:
    """Create an AuthStatus for testing.

    Args:
        status: Authentication status type.
        tenant_name: Name of the tenant.
        tenant_id: Tenant ID (if authenticated).
        expires_at: Token expiration time (if authenticated).

    Returns:
        An AuthStatus instance with the specified attributes.

    Example:
        status = make_auth_status(
            status=AuthStatusType.EXPIRED,
            tenant_name="contoso"
        )
    """
    if expires_at is None and status == AuthStatusType.AUTHENTICATED:
        expires_at = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
    return AuthStatus(
        status=status,
        tenant_name=tenant_name,
        tenant_id=tenant_id if status != AuthStatusType.UNAUTHENTICATED else None,
        expires_at=expires_at,
    )


def make_cached_token(
    access_token: str = "test-access-token",
    tenant_id: str = "12345-tenant-id",
    expires_at: datetime | None = None,
    scopes: list[str] | None = None,
) -> CachedToken:
    """Create a CachedToken for testing.

    Args:
        access_token: The access token string.
        tenant_id: Tenant ID the token is for.
        expires_at: Token expiration time.
        scopes: List of OAuth scopes.

    Returns:
        A CachedToken instance with the specified attributes.

    Example:
        token = make_cached_token(tenant_id="contoso-123")
    """
    if expires_at is None:
        expires_at = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
    if scopes is None:
        scopes = ["https://graph.microsoft.com/.default"]
    return CachedToken(
        access_token=access_token,
        expires_at=expires_at,
        tenant_id=tenant_id,
        scopes=scopes,
    )


def make_policy_assignment(
    policy_type: str = "TeamsCallingPolicy",
    display_name: str = "AllowCalling",
    assignment_type: str = "direct",
    policy_id: str = "policy-11111111-1111-1111-1111-111111111111",
    group_id: str | None = None,
) -> EffectivePolicyAssignment:
    """Create an EffectivePolicyAssignment for testing.

    Args:
        policy_type: Type of policy.
        display_name: Policy display name.
        assignment_type: How the policy was assigned (direct, group, global).
        policy_id: Unique policy identifier.
        group_id: Group ID if assigned via group (optional).

    Returns:
        An EffectivePolicyAssignment instance with the specified attributes.

    Example:
        assignment = make_policy_assignment(
            assignment_type="group",
            group_id="group-123"
        )
    """
    data: dict[str, Any] = {
        "policyType": policy_type,
        "policyAssignment": {
            "displayName": display_name,
            "assignmentType": assignment_type,
            "policyId": policy_id,
        },
    }
    if group_id is not None:
        data["policyAssignment"]["groupId"] = group_id
    return EffectivePolicyAssignment.model_validate(data)


# =============================================================================
# GraphClient Async Context Manager Fixture
# =============================================================================


@pytest.fixture
def mock_graph_client_context() -> Generator[tuple[MagicMock, AsyncMock], None, None]:
    """Create a mock GraphClient with async context manager support.

    This fixture provides a properly configured mock for GraphClient that
    supports the async context manager protocol (async with).

    Yields:
        A tuple of (mock_class, mock_instance) where:
        - mock_class: The patched GraphClient class
        - mock_instance: The mock instance returned by the context manager

    Example:
        def test_with_graph_client(mock_graph_client_context):
            mock_class, mock_gc = mock_graph_client_context
            # mock_gc is what you get from `async with GraphClient() as gc:`
    """
    mock_gc = AsyncMock()
    mock_gc.__aenter__.return_value = mock_gc
    mock_gc.__aexit__.return_value = None
    yield MagicMock(return_value=mock_gc), mock_gc


# =============================================================================
# Individual Service Mock Fixtures
# =============================================================================


@pytest.fixture
def mock_cache_manager() -> MagicMock:
    """Create a mock CacheManager.

    Returns:
        A MagicMock configured as a CacheManager.
    """
    return MagicMock()


@pytest.fixture
def mock_auth_service() -> MagicMock:
    """Create a mock AuthService.

    Returns:
        A MagicMock configured as an AuthService with common method stubs.
    """
    mock = MagicMock()
    mock.get_status.return_value = make_auth_status()
    mock.login.return_value = make_cached_token()
    return mock


@pytest.fixture
def mock_user_service() -> MagicMock:
    """Create a mock UserService with async methods.

    Returns:
        A MagicMock configured as a UserService with AsyncMock methods.
    """
    mock = MagicMock()
    mock.list_users = AsyncMock(return_value=[])
    mock.resolve_user = AsyncMock(return_value=make_user_config())
    mock.search_users = AsyncMock(return_value=[])
    return mock


@pytest.fixture
def mock_number_service() -> MagicMock:
    """Create a mock NumberService with async methods.

    Returns:
        A MagicMock configured as a NumberService with AsyncMock methods.
    """
    mock = MagicMock()
    mock.list_numbers = AsyncMock(return_value=[])
    mock.get_number = AsyncMock(return_value=make_number())
    mock.search_numbers = AsyncMock(return_value=[])
    mock.assign_number = AsyncMock(return_value=None)
    mock.unassign_number = AsyncMock(return_value=None)
    return mock


@pytest.fixture
def mock_location_service() -> MagicMock:
    """Create a mock LocationService (sync).

    Returns:
        A MagicMock configured as a LocationService with sync method stubs.
    """
    mock = MagicMock()
    mock.list_locations.return_value = []
    mock.validate_location.return_value = make_location()
    mock.get_staleness_warning.return_value = None
    return mock


@pytest.fixture
def mock_policy_service() -> MagicMock:
    """Create a mock PolicyService (sync, cache-based).

    Returns:
        A MagicMock configured as a PolicyService with sync method stubs.
    """
    mock = MagicMock()
    mock.list_policies.return_value = []
    mock.validate_policy.return_value = make_policy()
    mock.get_staleness_warning.return_value = None
    return mock


@pytest.fixture
def mock_tenant_service() -> MagicMock:
    """Create a mock TenantService.

    Returns:
        A MagicMock configured as a TenantService with common method stubs.
    """
    mock = MagicMock()
    mock.list_tenants.return_value = []
    mock.get_current_tenant.return_value = make_tenant()
    mock.get_tenant.return_value = make_tenant()
    return mock


# =============================================================================
# Composite Fixtures for Common Test Scenarios
# =============================================================================


@pytest.fixture
def mock_cli_services_users(
    mock_cache_manager: MagicMock,
    mock_auth_service: MagicMock,
    mock_user_service: MagicMock,
) -> Generator[dict[str, MagicMock], None, None]:
    """Patch all services needed for users CLI commands.

    This fixture patches CacheManager, AuthService, GraphClient, and UserService
    at the teams_phone.cli.users module level.

    Yields:
        A dict containing all mock instances keyed by service name.

    Example:
        def test_users_list(runner, mock_cli_services_users):
            mocks = mock_cli_services_users
            mocks["user_service"].list_users.return_value = [make_user_config()]
            result = runner.invoke(app, ["users", "list"])
            assert result.exit_code == 0
    """
    mock_gc = AsyncMock()
    mock_gc.__aenter__.return_value = mock_gc
    mock_gc.__aexit__.return_value = None

    with (
        patch("teams_phone.cli.users.CacheManager", return_value=mock_cache_manager),
        patch("teams_phone.cli.users.AuthService", return_value=mock_auth_service),
        patch("teams_phone.cli.users.GraphClient", return_value=mock_gc),
        patch("teams_phone.cli.users.UserService", return_value=mock_user_service),
    ):
        yield {
            "cache_manager": mock_cache_manager,
            "auth_service": mock_auth_service,
            "graph_client": mock_gc,
            "user_service": mock_user_service,
        }


@pytest.fixture
def mock_cli_services_numbers(
    mock_cache_manager: MagicMock,
    mock_auth_service: MagicMock,
    mock_number_service: MagicMock,
) -> Generator[dict[str, MagicMock], None, None]:
    """Patch all services needed for numbers CLI commands.

    Yields:
        A dict containing all mock instances keyed by service name.
    """
    mock_gc = AsyncMock()
    mock_gc.__aenter__.return_value = mock_gc
    mock_gc.__aexit__.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
        ),
    ):
        yield {
            "cache_manager": mock_cache_manager,
            "auth_service": mock_auth_service,
            "graph_client": mock_gc,
            "number_service": mock_number_service,
        }


@pytest.fixture
def mock_cli_services_locations(
    mock_cache_manager: MagicMock,
    mock_location_service: MagicMock,
) -> Generator[dict[str, MagicMock], None, None]:
    """Patch all services needed for locations CLI commands.

    Yields:
        A dict containing all mock instances keyed by service name.
    """
    with (
        patch(
            "teams_phone.cli.locations.CacheManager", return_value=mock_cache_manager
        ),
        patch(
            "teams_phone.cli.locations.LocationService",
            return_value=mock_location_service,
        ),
    ):
        yield {
            "cache_manager": mock_cache_manager,
            "location_service": mock_location_service,
        }


@pytest.fixture
def mock_cli_services_policies(
    mock_policy_service: MagicMock,
) -> Generator[dict[str, MagicMock], None, None]:
    """Patch all services needed for policies CLI commands.

    Note: Policies CLI uses _get_sync_service helper, so we patch that.

    Yields:
        A dict containing all mock instances keyed by service name.
    """
    with patch(
        "teams_phone.cli.policies._get_sync_service",
        return_value=mock_policy_service,
    ):
        yield {
            "policy_service": mock_policy_service,
        }


@pytest.fixture
def mock_cli_services_tenants(
    mock_cache_manager: MagicMock,
    mock_tenant_service: MagicMock,
) -> Generator[dict[str, MagicMock], None, None]:
    """Patch all services needed for tenants CLI commands.

    Yields:
        A dict containing all mock instances keyed by service name.
    """
    with (
        patch("teams_phone.cli.tenants.CacheManager", return_value=mock_cache_manager),
        patch(
            "teams_phone.cli.tenants.TenantService", return_value=mock_tenant_service
        ),
    ):
        yield {
            "cache_manager": mock_cache_manager,
            "tenant_service": mock_tenant_service,
        }


@pytest.fixture
def mock_cli_services_auth(
    mock_cache_manager: MagicMock,
    mock_auth_service: MagicMock,
) -> Generator[dict[str, MagicMock], None, None]:
    """Patch all services needed for auth CLI commands.

    Yields:
        A dict containing all mock instances keyed by service name.
    """
    with (
        patch("teams_phone.cli.auth.CacheManager", return_value=mock_cache_manager),
        patch("teams_phone.cli.auth.AuthService", return_value=mock_auth_service),
    ):
        yield {
            "cache_manager": mock_cache_manager,
            "auth_service": mock_auth_service,
        }
