"""Unit tests for Defender for Cloud enumerator."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from azure.core.exceptions import HttpResponseError

from azure_discovery.enumerators.defender import DefenderEnumerator
from azure_discovery.adt_types.models import ResourceNode


async def _async_iter(*items):
    """Return an async iterable over items (for mocking client.alerts.list etc.)."""
    for item in items:
        yield item


@pytest.fixture
def mock_credential():
    """Mock Azure credential."""
    return AsyncMock()


@pytest.fixture
def defender_enumerator(mock_credential):
    """Create DefenderEnumerator instance."""
    return DefenderEnumerator(
        credential=mock_credential,
        base_url="https://management.azure.com",
    )


@pytest.fixture
def mock_alert():
    """Create a mock security alert."""
    alert = MagicMock()
    alert.id = "/subscriptions/sub1/providers/Microsoft.Security/locations/centralus/alerts/alert1"
    alert.name = "alert1"
    alert.properties = MagicMock()
    alert.properties.alert_display_name = "Suspicious PowerShell execution"
    alert.properties.severity = "High"
    alert.properties.status = "Active"
    alert.properties.intent = "Execution"
    alert.properties.description = "Suspicious PowerShell activity detected"
    alert.properties.start_time_utc = None
    alert.properties.end_time_utc = None
    alert.properties.time_generated_utc = None
    alert.properties.compromised_entity = "VM-WEB-01"
    alert.properties.remediation_steps = ["Isolate the VM", "Review logs"]
    alert.properties.vendor_name = "Microsoft"
    alert.properties.alert_type = "VM_SuspiciousPowerShell"
    alert.properties.alert_uri = "https://portal.azure.com/#alert/alert1"
    alert.properties.techniques = ["T1059.001"]
    alert.properties.tactics = ["Execution"]
    alert.properties.entities = [
        {"id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/VM-WEB-01"}
    ]
    alert.properties.extended_properties = {"source": "Defender for Cloud"}
    return alert


@pytest.fixture
def mock_assessment():
    """Create a mock security assessment."""
    assessment = MagicMock()
    assessment.id = "/subscriptions/sub1/providers/Microsoft.Security/assessments/assessment1"
    assessment.name = "assessment1"
    assessment.properties = MagicMock()
    assessment.properties.display_name = "Disk encryption should be applied"
    assessment.properties.status = MagicMock()
    assessment.properties.status.code = "Unhealthy"
    assessment.properties.status.cause = None
    assessment.properties.status.description = "Disk encryption is not enabled"
    assessment.properties.metadata = MagicMock()
    assessment.properties.metadata.severity = "Medium"
    assessment.properties.metadata.assessment_type = "BuiltIn"
    assessment.properties.metadata.description = "Enable disk encryption for data protection"
    assessment.properties.metadata.remediation_description = "Enable Azure Disk Encryption"
    assessment.properties.metadata.categories = ["Data"]
    assessment.properties.metadata.threats = ["Data Breach"]
    assessment.properties.resource_details = MagicMock()
    assessment.properties.resource_details.id = "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/VM-DB-01"
    assessment.properties.additional_data = {"diskNames": ["osdisk"]}
    return assessment


@pytest.fixture
def mock_secure_score():
    """Create a mock secure score."""
    score = MagicMock()
    score.id = "/subscriptions/sub1/providers/Microsoft.Security/secureScores/ascScore"
    score.name = "ascScore"
    score.properties = MagicMock()
    score.properties.display_name = "ASC score"
    score.properties.score = MagicMock()
    score.properties.score.current = 42.5
    score.properties.score.max = 100.0
    score.properties.score.percentage = 0.425
    score.properties.weight = 100
    return score


def _make_mock_client():
    """Client mock where .list() returns an async iterable (use MagicMock, not AsyncMock)."""
    client = MagicMock()
    client.close = AsyncMock(return_value=None)
    return client


@pytest.mark.asyncio
async def test_enumerate_security_alerts_success(defender_enumerator, mock_alert):
    """Test successful enumeration of security alerts."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        mock_client.alerts.list.return_value = _async_iter(mock_alert)

        alerts = await defender_enumerator.enumerate_security_alerts(
            subscription_ids=["sub1"],
        )

        assert len(alerts) == 1
        alert_node = alerts[0]
        assert isinstance(alert_node, ResourceNode)
        assert alert_node.type == "Microsoft.Security/alerts"
        assert alert_node.properties["alertDisplayName"] == "Suspicious PowerShell execution"
        assert alert_node.properties["severity"] == "High"
        assert alert_node.properties["status"] == "Active"
        assert alert_node.properties["techniques"] == ["T1059.001"]
        assert alert_node.properties["tactics"] == ["Execution"]
        assert len(alert_node.properties["affectedResources"]) == 1

        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_security_alerts_with_severity_filter(defender_enumerator, mock_alert):
    """Test alert enumeration with severity filter."""
    low_alert = MagicMock()
    low_alert.properties = MagicMock()
    low_alert.properties.severity = "Low"
    low_alert.properties.status = "Active"

    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        mock_client.alerts.list.return_value = _async_iter(mock_alert, low_alert)

        alerts = await defender_enumerator.enumerate_security_alerts(
            subscription_ids=["sub1"],
            severity_filter=["High", "Medium"],
        )

        # Should only return the High severity alert
        assert len(alerts) == 1
        assert alerts[0].properties["severity"] == "High"


@pytest.mark.asyncio
async def test_enumerate_security_alerts_with_status_filter(defender_enumerator, mock_alert):
    """Test alert enumeration with status filter."""
    resolved_alert = MagicMock()
    resolved_alert.properties = MagicMock()
    resolved_alert.properties.severity = "High"
    resolved_alert.properties.status = "Resolved"

    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        mock_client.alerts.list.return_value = _async_iter(mock_alert, resolved_alert)

        alerts = await defender_enumerator.enumerate_security_alerts(
            subscription_ids=["sub1"],
            status_filter=["Active"],
        )

        # Should only return the Active alert
        assert len(alerts) == 1
        assert alerts[0].properties["status"] == "Active"


@pytest.mark.asyncio
async def test_enumerate_security_alerts_defender_not_enabled(defender_enumerator):
    """Test alert enumeration when Defender is not enabled."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        
        # Simulate 404 error when Defender is not enabled
        error = HttpResponseError()
        error.status_code = 404
        mock_client.alerts.list.side_effect = error

        alerts = await defender_enumerator.enumerate_security_alerts(
            subscription_ids=["sub1"],
        )

        # Should return empty list, not raise exception
        assert len(alerts) == 0
        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_security_assessments_success(defender_enumerator, mock_assessment):
    """Test successful enumeration of security assessments."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        mock_client.assessments.list.return_value = _async_iter(mock_assessment)

        assessments = await defender_enumerator.enumerate_security_assessments(
            subscription_ids=["sub1"],
        )

        assert len(assessments) == 1
        assessment_node = assessments[0]
        assert isinstance(assessment_node, ResourceNode)
        assert assessment_node.type == "Microsoft.Security/assessments"
        assert assessment_node.properties["displayName"] == "Disk encryption should be applied"
        assert assessment_node.properties["statusCode"] == "Unhealthy"
        assert assessment_node.properties["severity"] == "Medium"
        assert assessment_node.properties["affectedResourceId"].endswith("VM-DB-01")

        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_security_assessments_with_filters(defender_enumerator, mock_assessment):
    """Test assessment enumeration with severity and status filters."""
    healthy_assessment = MagicMock()
    healthy_assessment.properties = MagicMock()
    healthy_assessment.properties.status = MagicMock()
    healthy_assessment.properties.status.code = "Healthy"
    healthy_assessment.properties.metadata = MagicMock()
    healthy_assessment.properties.metadata.severity = "Medium"

    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        mock_client.assessments.list.return_value = _async_iter(
            mock_assessment,
            healthy_assessment,
        )

        assessments = await defender_enumerator.enumerate_security_assessments(
            subscription_ids=["sub1"],
            severity_filter=["High", "Medium"],
            status_filter=["Unhealthy"],
        )

        # Should only return the Unhealthy Medium severity assessment
        assert len(assessments) == 1
        assert assessments[0].properties["statusCode"] == "Unhealthy"


@pytest.mark.asyncio
async def test_enumerate_secure_scores_success(defender_enumerator, mock_secure_score):
    """Test successful enumeration of secure scores."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        mock_client.secure_scores.get = AsyncMock(return_value=mock_secure_score)

        scores = await defender_enumerator.enumerate_secure_scores(
            subscription_ids=["sub1"],
        )

        assert len(scores) == 1
        score_node = scores[0]
        assert isinstance(score_node, ResourceNode)
        assert score_node.type == "Microsoft.Security/secureScores"
        assert score_node.properties["displayName"] == "ASC score"
        assert score_node.properties["currentScore"] == 42.5
        assert score_node.properties["maxScore"] == 100.0
        assert score_node.properties["percentage"] == 0.425

        mock_client.secure_scores.get.assert_called_once_with(secure_score_name="ascScore")
        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_secure_scores_not_available(defender_enumerator):
    """Test secure score enumeration when score is not available."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        
        # Simulate 404 error when secure score is not available
        error = HttpResponseError()
        error.status_code = 404
        mock_client.secure_scores.get.side_effect = error

        scores = await defender_enumerator.enumerate_secure_scores(
            subscription_ids=["sub1"],
        )

        # Should return empty list, not raise exception
        assert len(scores) == 0
        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_multiple_subscriptions(defender_enumerator, mock_alert):
    """Test enumeration across multiple subscriptions."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        # Each subscription gets its own async iterator (list() called once per sub)
        mock_client.alerts.list.side_effect = [
            _async_iter(mock_alert),
            _async_iter(mock_alert),
            _async_iter(mock_alert),
        ]

        alerts = await defender_enumerator.enumerate_security_alerts(
            subscription_ids=["sub1", "sub2", "sub3"],
        )

        # Should create 3 alerts (one per subscription)
        assert len(alerts) == 3

        # Verify client was created 3 times (once per subscription)
        assert mock_client_class.call_count == 3


@pytest.mark.asyncio
async def test_enumerate_security_alerts_non_404_http_error(defender_enumerator):
    """Test alert enumeration when list() raises non-404 HttpResponseError."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        error = HttpResponseError()
        error.status_code = 500
        mock_client.alerts.list.side_effect = error

        alerts = await defender_enumerator.enumerate_security_alerts(
            subscription_ids=["sub1"],
        )

        assert len(alerts) == 0
        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_security_assessments_non_404_http_error(defender_enumerator):
    """Test assessment enumeration when list() raises non-404 HttpResponseError."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        error = HttpResponseError()
        error.status_code = 403
        mock_client.assessments.list.side_effect = error

        assessments = await defender_enumerator.enumerate_security_assessments(
            subscription_ids=["sub1"],
        )

        assert len(assessments) == 0
        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_secure_scores_get_returns_404(defender_enumerator):
    """Test secure score when get() raises 404 (score not available)."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        error = HttpResponseError()
        error.status_code = 404
        mock_client.secure_scores.get = AsyncMock(side_effect=error)

        scores = await defender_enumerator.enumerate_secure_scores(
            subscription_ids=["sub1"],
        )

        assert len(scores) == 0
        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_secure_scores_get_returns_non_404_http_error(defender_enumerator):
    """Test secure score when get() raises non-404 HttpResponseError (covers inner except debug)."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        error = HttpResponseError()
        error.status_code = 500
        mock_client.secure_scores.get = AsyncMock(side_effect=error)

        scores = await defender_enumerator.enumerate_secure_scores(
            subscription_ids=["sub1"],
        )

        assert len(scores) == 0
        mock_client.close.assert_called_once()


@pytest.mark.asyncio
async def test_enumerate_secure_scores_outer_exception(defender_enumerator):
    """Test secure score when get() raises generic Exception (covers outer except)."""
    with patch("azure_discovery.enumerators.defender.SecurityCenter") as mock_client_class:
        mock_client = _make_mock_client()
        mock_client_class.return_value = mock_client
        mock_client.secure_scores.get = AsyncMock(side_effect=RuntimeError("network error"))

        scores = await defender_enumerator.enumerate_secure_scores(
            subscription_ids=["sub1"],
        )

        assert len(scores) == 0
        mock_client.close.assert_called_once()
