"""Contract tests for user-related Pydantic models.

These tests validate that UserConfiguration, TelephoneNumber, EffectivePolicyAssignment,
and PolicyAssignmentDetail models correctly parse live Graph API responses.

Uses extra='forbid' validation to detect API contract drift - any unexpected field
in the API response will cause a ValidationError.
"""

from __future__ import annotations

from typing import Any

import pytest

from teams_phone.constants import GRAPH_API_V1_BASE
from teams_phone.infrastructure.graph_client import GraphClient
from teams_phone.models import (
    EffectivePolicyAssignment,
    PolicyAssignmentDetail,
    TelephoneNumber,
    UserConfiguration,
)


def _strip_odata_metadata(data: dict[str, Any]) -> dict[str, Any]:
    """Strip OData metadata fields from a response dict.

    Graph API responses include @odata.context and other @ prefixed fields
    that are not part of the actual data contract. This helper removes them
    before Pydantic validation with extra="forbid".
    """
    return {k: v for k, v in data.items() if not k.startswith("@")}


@pytest.mark.contract
@pytest.mark.asyncio
async def test_user_configuration_schema(live_graph_client: GraphClient) -> None:
    """Validate UserConfiguration model against live /admin/teams/userConfigurations endpoint.

    Fetches a small batch of users and validates each against the UserConfiguration
    Pydantic model. Uses extra='forbid' to catch any unexpected fields from the API.
    """
    # GraphClient.get() returns dict directly (handles errors internally)
    data = await live_graph_client.get("/admin/teams/userConfigurations?$top=5")
    users = data.get("value", [])

    if not users:
        pytest.skip("No user configurations available in test tenant")

    # Validate each user configuration
    validated_users = []
    for user_data in users:
        # Strip OData metadata before validation
        clean_data = _strip_odata_metadata(user_data)
        user = UserConfiguration.model_validate(clean_data)
        validated_users.append(user)

    # Basic sanity checks on validated data
    assert len(validated_users) == len(users)
    for user in validated_users:
        assert user.id  # ID should be present
        assert user.user_principal_name  # UPN should be present
        assert user.tenant_id  # Tenant ID should be present


@pytest.mark.contract
@pytest.mark.asyncio
async def test_telephone_number_nested_model(live_graph_client: GraphClient) -> None:
    """Validate TelephoneNumber nested model parsing in UserConfiguration.

    Fetches users and finds those with assigned telephone numbers to validate
    the nested TelephoneNumber model structure.
    """
    # Fetch more users to increase chance of finding ones with phone numbers
    data = await live_graph_client.get("/admin/teams/userConfigurations?$top=20")
    users = data.get("value", [])

    if not users:
        pytest.skip("No user configurations available in test tenant")

    # Find users with telephone numbers
    users_with_phones = []
    for user_data in users:
        clean_data = _strip_odata_metadata(user_data)
        user = UserConfiguration.model_validate(clean_data)
        if user.telephone_numbers:
            users_with_phones.append(user)

    if not users_with_phones:
        pytest.skip("No users with telephone numbers found in test tenant")

    # Validate the TelephoneNumber nested model structure
    for user in users_with_phones:
        for phone in user.telephone_numbers:
            # Validate that TelephoneNumber was parsed correctly
            assert isinstance(phone, TelephoneNumber)
            assert phone.telephone_number  # Should have a phone number
            assert phone.assignment_category  # Should have assignment category


@pytest.mark.contract
@pytest.mark.asyncio
async def test_effective_policy_assignment_nested_model(
    live_graph_client: GraphClient,
) -> None:
    """Validate EffectivePolicyAssignment and PolicyAssignmentDetail nested models.

    Fetches users and finds those with policy assignments to validate the nested
    policy assignment model structure.
    """
    data = await live_graph_client.get("/admin/teams/userConfigurations?$top=20")
    users = data.get("value", [])

    if not users:
        pytest.skip("No user configurations available in test tenant")

    # Find users with policy assignments
    users_with_policies = []
    for user_data in users:
        clean_data = _strip_odata_metadata(user_data)
        user = UserConfiguration.model_validate(clean_data)
        if user.effective_policy_assignments:
            users_with_policies.append(user)

    if not users_with_policies:
        pytest.skip("No users with policy assignments found in test tenant")

    # Validate the EffectivePolicyAssignment and PolicyAssignmentDetail models
    for user in users_with_policies:
        for policy in user.effective_policy_assignments:
            # Validate EffectivePolicyAssignment structure
            assert isinstance(policy, EffectivePolicyAssignment)
            assert policy.policy_type  # Should have a policy type
            assert policy.policy_assignment  # Should have assignment details

            # Validate PolicyAssignmentDetail structure
            assert isinstance(policy.policy_assignment, PolicyAssignmentDetail)
            assert policy.policy_assignment.display_name  # Should have display name
            assert (
                policy.policy_assignment.assignment_type
            )  # Should have assignment type
            assert policy.policy_assignment.policy_id  # Should have policy ID


@pytest.mark.contract
@pytest.mark.asyncio
async def test_user_directory_lookup(live_graph_client: GraphClient) -> None:
    """Validate directory lookup using v1.0 /users endpoint.

    Tests the standard Graph API /users endpoint which is used for display name
    enrichment and user lookups. Validates the response structure contains
    expected fields.
    """
    # Use the v1.0 API base for directory operations via get_paginated_url
    # which accepts full URLs (unlike get() which always uses beta)
    url = f"{GRAPH_API_V1_BASE}/users"
    params = {"$top": "5", "$select": "id,displayName,userPrincipalName"}

    users = []
    async for user in live_graph_client.get_paginated_url(
        url, params=params, max_items=5
    ):
        users.append(user)

    if not users:
        pytest.skip("No users available in test tenant directory")

    # Validate expected fields are present
    for user in users:
        assert "id" in user, "User should have 'id' field"
        assert "userPrincipalName" in user, "User should have 'userPrincipalName' field"
        # displayName may be null for some users, but the field should exist
        assert "displayName" in user, "User should have 'displayName' field"


@pytest.mark.contract
@pytest.mark.asyncio
async def test_odata_metadata_stripping(live_graph_client: GraphClient) -> None:
    """Verify that @odata.* metadata fields are handled correctly.

    The Graph API includes @odata.context, @odata.type, and other metadata fields
    in responses. These must be stripped before validation with extra='forbid' models,
    otherwise validation will fail.
    """
    # GraphClient.get() returns dict directly
    data = await live_graph_client.get("/admin/teams/userConfigurations?$top=1")

    # Verify that OData metadata is present in raw response
    assert "@odata.context" in data, "API response should include @odata.context"

    users = data.get("value", [])
    if not users:
        pytest.skip("No user configurations available in test tenant")

    user_data = users[0]

    # Check if user data contains any @odata.* fields
    odata_fields = [k for k in user_data.keys() if k.startswith("@")]

    # Strip the metadata
    clean_data = _strip_odata_metadata(user_data)

    # Verify no @odata.* fields remain
    remaining_odata = [k for k in clean_data.keys() if k.startswith("@")]
    assert not remaining_odata, f"Metadata not fully stripped: {remaining_odata}"

    # Validation should succeed after stripping
    user = UserConfiguration.model_validate(clean_data)
    assert user.id  # Sanity check that we got valid data

    # Log info about what was stripped (useful for debugging)
    if odata_fields:
        # This is expected - just confirming we handle it correctly
        pass
