"""Integration tests for sensitive data masking in the span export pipeline."""

from unittest.mock import MagicMock, patch

import pytest

from lumenova_beacon import BeaconClient
from lumenova_beacon.tracing.span import Span
from lumenova_beacon.types import SpanKind, SpanType


def _redact_emails(value):
    """Simple masking function that replaces email-like strings."""
    if isinstance(value, str) and "@" in value:
        return "<REDACTED_EMAIL>"
    return value


def _create_test_span(**kwargs):
    """Create a span with test attributes containing sensitive data."""
    span = Span(
        name=kwargs.get("name", "test-span"),
        span_type=SpanType.SPAN,
        kind=SpanKind.INTERNAL,
    )
    span.start()
    span.set_attribute("user.email", "alice@example.com")
    span.set_attribute("user.query", "Hello from alice@example.com")
    span.set_attribute("span.type", "span")
    span.end()
    return span


def _create_client(masking_function=None, database_callback=None):
    """Create a BeaconClient configured for testing."""
    return BeaconClient(
        endpoint="https://test.example.com",
        api_key="test-key",
        auto_instrument_opentelemetry=False,
        masking_function=masking_function,
        database_callback=database_callback,
    )


class TestExportSpanMasking:
    """Test that export_span() applies masking when configured."""

    def test_export_span_applies_masking_with_database_callback(self):
        """Masking should be applied before the database callback receives the span."""
        captured_spans = []

        def capture_callback(span):
            captured_spans.append(span)

        client = _create_client(
            masking_function=_redact_emails,
            database_callback=capture_callback,
        )

        span = _create_test_span()
        client.export_span(span)

        assert len(captured_spans) == 1
        exported = captured_spans[0]
        assert exported.attributes["user.email"] == "<REDACTED_EMAIL>"
        assert exported.attributes["user.query"] == "<REDACTED_EMAIL>"

    def test_export_span_no_masking_when_function_is_none(self):
        """When masking_function is None, spans should pass through unmodified."""
        captured_spans = []

        def capture_callback(span):
            captured_spans.append(span)

        client = _create_client(
            masking_function=None,
            database_callback=capture_callback,
        )

        span = _create_test_span()
        client.export_span(span)

        assert len(captured_spans) == 1
        exported = captured_spans[0]
        assert exported.attributes["user.email"] == "alice@example.com"
        assert exported.attributes["user.query"] == "Hello from alice@example.com"

    def test_export_span_calls_apply_masking_before_otel_path(self):
        """Masking should be invoked in the non-callback (OTEL) export path."""
        client = _create_client(masking_function=_redact_emails)

        span = _create_test_span()

        with patch.object(client, '_apply_masking_to_span', wraps=client._apply_masking_to_span) as mock_mask:
            # export_span will fail at the OTEL provider step (no provider configured),
            # but masking should still be called before that point
            client.export_span(span)
            mock_mask.assert_called_once_with(span)


class TestExportEagerSpanMasking:
    """Test that export_eager_span() applies masking when configured."""

    def test_export_eager_span_applies_masking(self):
        """Masking should be invoked in the eager export path."""
        client = _create_client(masking_function=_redact_emails)
        client.config.eager_export = True

        span = _create_test_span()

        with patch.object(client, '_apply_masking_to_span', wraps=client._apply_masking_to_span) as mock_mask:
            # Will fail at OTEL provider step, but masking should be called first
            client.export_eager_span(span)
            mock_mask.assert_called_once_with(span)

    def test_export_eager_span_no_masking_when_disabled(self):
        """When eager_export is False, export_eager_span should return False."""
        client = _create_client(masking_function=_redact_emails)
        client.config.eager_export = False

        span = _create_test_span()
        result = client.export_eager_span(span)
        assert result is False


class TestApplyMaskingToSpan:
    """Test that _apply_masking_to_span does not mutate the original span."""

    def test_original_span_not_mutated(self):
        """_apply_masking_to_span should return a copy, leaving the original intact."""
        client = _create_client(masking_function=_redact_emails)

        span = _create_test_span()
        original_email = span.attributes["user.email"]
        original_query = span.attributes["user.query"]

        masked_span = client._apply_masking_to_span(span)

        # Original span should be unchanged
        assert span.attributes["user.email"] == original_email
        assert span.attributes["user.query"] == original_query

        # Masked span should have redacted values
        assert masked_span.attributes["user.email"] == "<REDACTED_EMAIL>"
        assert masked_span.attributes["user.query"] == "<REDACTED_EMAIL>"

    def test_masked_span_preserves_identity_fields(self):
        """Technical fields like trace_id, span_id should be preserved."""
        client = _create_client(masking_function=_redact_emails)

        span = _create_test_span()
        masked_span = client._apply_masking_to_span(span)

        assert masked_span.trace_id == span.trace_id
        assert masked_span.span_id == span.span_id
        assert masked_span.parent_id == span.parent_id
        assert masked_span.name == span.name

    def test_masked_span_is_different_object(self):
        """The masked span should be a different object from the original."""
        client = _create_client(masking_function=_redact_emails)

        span = _create_test_span()
        masked_span = client._apply_masking_to_span(span)

        assert masked_span is not span
        assert masked_span.attributes is not span.attributes


class TestDatabaseCallbackReceivesMaskedData:
    """Test that database_callback receives masked data when masking is configured."""

    def test_callback_gets_masked_span(self):
        """The database callback should receive a span with masked attributes."""
        received = []

        def db_callback(span):
            received.append({
                "email": span.attributes.get("user.email"),
                "query": span.attributes.get("user.query"),
            })

        client = _create_client(
            masking_function=_redact_emails,
            database_callback=db_callback,
        )

        span = _create_test_span()
        client.export_span(span)

        assert len(received) == 1
        assert received[0]["email"] == "<REDACTED_EMAIL>"
        assert received[0]["query"] == "<REDACTED_EMAIL>"

    def test_callback_gets_unmasked_span_without_masking_function(self):
        """Without masking, the database callback receives original data."""
        received = []

        def db_callback(span):
            received.append({
                "email": span.attributes.get("user.email"),
                "query": span.attributes.get("user.query"),
            })

        client = _create_client(
            masking_function=None,
            database_callback=db_callback,
        )

        span = _create_test_span()
        client.export_span(span)

        assert len(received) == 1
        assert received[0]["email"] == "alice@example.com"
        assert received[0]["query"] == "Hello from alice@example.com"
