"""Tests for Entra ID enumeration."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from azure_discovery.adt_types import AzureDiscoveryRequest, AzureEnvironment
from azure_discovery.enumerators.entra import enumerate_entra_resources


@pytest.mark.asyncio
async def test_enumerate_entra_resources_maps_nodes() -> None:
    mock_credential = MagicMock()

    with patch("msgraph.GraphServiceClient") as MockClient:
        client_instance = MockClient.return_value

        client_instance.identity = MagicMock()
        client_instance.identity.conditional_access = MagicMock()
        client_instance.identity.conditional_access.policies = MagicMock()

        client_instance.identity_protection = MagicMock()
        client_instance.identity_protection.risky_users = MagicMock()

        async def fake_get_all_pages(*_, operation_name: str, **__):
            if operation_name == "entra.users":
                user = MagicMock()
                user.id = "user-1"
                user.display_name = "User One"
                user.user_principal_name = "user1@example.com"
                return [user]

            if operation_name == "entra.groups":
                group = MagicMock()
                group.id = "group-1"
                group.display_name = "Group One"
                group.group_types = ["Unified"]
                return [group]

            if operation_name == "entra.applications":
                app = MagicMock()
                app.id = "app-1"
                app.display_name = "App One"
                app.app_id = "app-guid-1"
                return [app]

            if operation_name == "entra.conditional_access_policies":
                policy = MagicMock()
                policy.id = "policy-1"
                policy.display_name = "Policy One"
                policy.state = "enabled"
                return [policy]

            return []

        with patch(
            "azure_discovery.enumerators.entra.get_all_pages",
            new=AsyncMock(side_effect=fake_get_all_pages),
        ):
            request = AzureDiscoveryRequest(
                tenant_id="test-tenant",
                environment=AzureEnvironment.AZURE_PUBLIC,
                include_relationships=False,
                entra_include_risky_users=False,
                entra_include_organization=False,
                entra_include_domains=False,
            )

            nodes, relationships = await enumerate_entra_resources(request, mock_credential)

    assert relationships == []
    assert len(nodes) == 4

    user_node = next(n for n in nodes if n.type == "Microsoft.Graph/User")
    assert user_node.id == "graph://user/user-1"
    assert user_node.name == "User One"
    assert user_node.subscription_id == "Tenant"
    assert user_node.tags["userPrincipalName"] == "user1@example.com"

    group_node = next(n for n in nodes if n.type == "Microsoft.Graph/Group")
    assert group_node.id == "graph://group/group-1"
    assert group_node.tags["groupType"] == "Unified"

    app_node = next(n for n in nodes if n.type == "Microsoft.Graph/Application")
    assert app_node.id == "graph://application/app-1"
    assert app_node.tags["appId"] == "app-guid-1"

    policy_node = next(
        n for n in nodes if n.type == "Microsoft.Graph/ConditionalAccessPolicy"
    )
    assert policy_node.id == "graph://conditionalAccessPolicy/policy-1"
    assert policy_node.tags["state"] == "enabled"


@pytest.mark.asyncio
async def test_enumerate_entra_resources_emits_appid_edges() -> None:
    mock_credential = MagicMock()

    with patch("msgraph.GraphServiceClient") as MockClient:
        client_instance = MockClient.return_value
        client_instance.identity = MagicMock()
        client_instance.identity.conditional_access = MagicMock()
        client_instance.identity.conditional_access.policies = MagicMock()

        client_instance.identity_protection = MagicMock()
        client_instance.identity_protection.risky_users = MagicMock()

        async def fake_get_all_pages(*_, operation_name: str, **__):
            if operation_name == "entra.applications":
                app = MagicMock()
                app.id = "app-1"
                app.display_name = "App One"
                app.app_id = "app-guid-1"
                return [app]

            if operation_name == "entra.service_principals":
                sp = MagicMock()
                sp.id = "sp-1"
                sp.display_name = "SP One"
                sp.app_id = "app-guid-1"
                return [sp]

            return []

        with patch(
            "azure_discovery.enumerators.entra.get_all_pages",
            new=AsyncMock(side_effect=fake_get_all_pages),
        ):
            request = AzureDiscoveryRequest(
                tenant_id="test-tenant",
                environment=AzureEnvironment.AZURE_PUBLIC,
                include_relationships=True,
                entra_include_users=False,
                entra_include_groups=False,
                entra_include_conditional_access_policies=False,
                entra_include_risky_users=False,
                entra_include_applications=True,
                entra_sp_ownership_max_sps=1,
                entra_sp_ownership_max_owners_per_sp=1,
                entra_include_organization=False,
                entra_include_domains=False,
                entra_group_membership_max_groups=0,
                entra_group_membership_max_members_per_group=0,
                entra_ownership_max_apps=0,
                entra_ownership_max_owners_per_app=0,
            )

            nodes, relationships = await enumerate_entra_resources(request, mock_credential)

    assert len(nodes) == 2
    assert len(relationships) == 1
    edge = relationships[0]
    assert edge.relation_type == "appId"
    assert edge.source_id == "graph://servicePrincipal/sp-1"
    assert edge.target_id == "graph://application/app-1"


@pytest.mark.asyncio
async def test_enumerate_entra_resources_emits_org_domain_edges() -> None:
    mock_credential = MagicMock()

    with patch("msgraph.GraphServiceClient") as MockClient:
        client_instance = MockClient.return_value
        client_instance.identity = MagicMock()
        client_instance.identity.conditional_access = MagicMock()
        client_instance.identity.conditional_access.policies = MagicMock()

        client_instance.identity_protection = MagicMock()
        client_instance.identity_protection.risky_users = MagicMock()

        async def fake_get_all_pages(*_, operation_name: str, **__):
            if operation_name == "entra.organization":
                org = MagicMock()
                org.id = "org-1"
                org.display_name = "Org One"
                org.tenant_type = "AAD"
                return [org]

            if operation_name == "entra.domains":
                d = MagicMock()
                d.id = "example.com"
                d.is_default = True
                return [d]

            return []

        with patch(
            "azure_discovery.enumerators.entra.get_all_pages",
            new=AsyncMock(side_effect=fake_get_all_pages),
        ):
            request = AzureDiscoveryRequest(
                tenant_id="test-tenant",
                environment=AzureEnvironment.AZURE_PUBLIC,
                include_relationships=True,
                entra_include_users=False,
                entra_include_groups=False,
                entra_include_applications=False,
                entra_include_conditional_access_policies=False,
                entra_include_risky_users=False,
                entra_include_organization=True,
                entra_include_domains=True,
                entra_sp_ownership_max_sps=0,
                entra_sp_ownership_max_owners_per_sp=0,
                entra_group_membership_max_groups=0,
                entra_group_membership_max_members_per_group=0,
                entra_ownership_max_apps=0,
                entra_ownership_max_owners_per_app=0,
            )

            nodes, relationships = await enumerate_entra_resources(request, mock_credential)

    assert len(nodes) == 2
    assert len(relationships) == 1
    assert relationships[0].relation_type == "has_domain"
    assert relationships[0].source_id == "graph://organization/org-1"
    assert relationships[0].target_id == "graph://domain/example.com"


@pytest.mark.asyncio
async def test_enumerate_entra_resources_expands_group_membership_bounded() -> None:
    mock_credential = MagicMock()

    with patch("msgraph.GraphServiceClient") as MockClient:
        client_instance = MockClient.return_value
        client_instance.identity = MagicMock()
        client_instance.identity.conditional_access = MagicMock()
        client_instance.identity.conditional_access.policies = MagicMock()

        client_instance.identity_protection = MagicMock()
        client_instance.identity_protection.risky_users = MagicMock()

        async def fake_get_all_pages(*_, operation_name: str, **__):
            if operation_name == "entra.groups":
                group = MagicMock()
                group.id = "group-1"
                group.display_name = "Group One"
                group.group_types = ["Unified"]
                return [group]

            if operation_name == "entra.group_members":
                member = MagicMock()
                member.id = "user-2"
                member.display_name = "User Two"
                member.odata_type = "#microsoft.graph.user"
                return [member]

            return []

        with patch(
            "azure_discovery.enumerators.entra.get_all_pages",
            new=AsyncMock(side_effect=fake_get_all_pages),
        ):
            request = AzureDiscoveryRequest(
                tenant_id="test-tenant",
                environment=AzureEnvironment.AZURE_PUBLIC,
                include_relationships=True,
                entra_include_organization=False,
                entra_include_domains=False,
                entra_include_users=False,
                entra_include_groups=True,
                entra_include_applications=False,
                entra_include_conditional_access_policies=False,
                entra_include_risky_users=False,
                entra_group_membership_max_groups=1,
                entra_group_membership_max_members_per_group=1,
                entra_ownership_max_apps=0,
                entra_ownership_max_owners_per_app=0,
                entra_sp_ownership_max_sps=0,
                entra_sp_ownership_max_owners_per_sp=0,
            )

            nodes, relationships = await enumerate_entra_resources(request, mock_credential)

    assert len(nodes) == 2
    assert any(n.id == "graph://group/group-1" for n in nodes)
    assert any(n.id == "graph://user/user-2" for n in nodes)

    assert len(relationships) == 1  # membership edge only (appId inference is a no-op here)
    assert any(
        r.relation_type == "has_member"
        and r.source_id == "graph://group/group-1"
        and r.target_id == "graph://user/user-2"
        for r in relationships
    )


@pytest.mark.asyncio
async def test_enumerate_entra_resources_expands_app_owners_bounded() -> None:
    mock_credential = MagicMock()

    with patch("msgraph.GraphServiceClient") as MockClient:
        client_instance = MockClient.return_value
        client_instance.identity = MagicMock()
        client_instance.identity.conditional_access = MagicMock()
        client_instance.identity.conditional_access.policies = MagicMock()

        client_instance.identity_protection = MagicMock()
        client_instance.identity_protection.risky_users = MagicMock()

        async def fake_get_all_pages(*_, operation_name: str, **__):
            if operation_name == "entra.applications":
                app = MagicMock()
                app.id = "app-1"
                app.display_name = "App One"
                app.app_id = "app-guid-1"
                return [app]

            if operation_name == "entra.application_owners":
                owner = MagicMock()
                owner.id = "user-3"
                owner.display_name = "Owner Three"
                owner.odata_type = "#microsoft.graph.user"
                return [owner]

            return []

        with patch(
            "azure_discovery.enumerators.entra.get_all_pages",
            new=AsyncMock(side_effect=fake_get_all_pages),
        ):
            request = AzureDiscoveryRequest(
                tenant_id="test-tenant",
                environment=AzureEnvironment.AZURE_PUBLIC,
                include_relationships=True,
                entra_include_organization=False,
                entra_include_domains=False,
                entra_include_users=False,
                entra_include_groups=False,
                entra_include_applications=True,
                entra_include_conditional_access_policies=False,
                entra_include_risky_users=False,
                entra_ownership_max_apps=1,
                entra_ownership_max_owners_per_app=1,
                entra_group_membership_max_groups=0,
                entra_group_membership_max_members_per_group=0,
                entra_sp_ownership_max_sps=0,
                entra_sp_ownership_max_owners_per_sp=0,
            )

            nodes, relationships = await enumerate_entra_resources(request, mock_credential)

    assert len(nodes) == 2
    assert any(n.id == "graph://application/app-1" for n in nodes)
    assert any(n.id == "graph://user/user-3" for n in nodes)

    assert any(
        r.relation_type == "has_owner"
        and r.source_id == "graph://application/app-1"
        and r.target_id == "graph://user/user-3"
        for r in relationships
    )
