import concurrent.futures as cf
import sys
from random import random
from unittest import mock

import pytest
from UnleashClient import UnleashClient

import sentry_sdk
from sentry_sdk import start_span, start_transaction
from sentry_sdk.integrations.unleash import UnleashIntegration
from tests.conftest import ApproxDict
from tests.integrations.unleash.testutils import mock_unleash_client


def test_is_enabled(sentry_init, capture_events, uninstall_integration):
    uninstall_integration(UnleashIntegration.identifier)

    with mock_unleash_client():
        client = UnleashClient()  # type: ignore[arg-type]
        sentry_init(integrations=[UnleashIntegration()])
        client.is_enabled("hello")
        client.is_enabled("world")
        client.is_enabled("other")

    events = capture_events()
    sentry_sdk.capture_exception(Exception("something wrong!"))

    assert len(events) == 1
    assert events[0]["contexts"]["flags"] == {
        "values": [
            {"flag": "hello", "result": True},
            {"flag": "world", "result": False},
            {"flag": "other", "result": False},
        ]
    }


def test_is_enabled_threaded(sentry_init, capture_events, uninstall_integration):
    uninstall_integration(UnleashIntegration.identifier)

    with mock_unleash_client():
        client = UnleashClient()  # type: ignore[arg-type]
        sentry_init(integrations=[UnleashIntegration()])
        events = capture_events()

        def task(flag_key):
            # Creates a new isolation scope for the thread.
            # This means the evaluations in each task are captured separately.
            with sentry_sdk.isolation_scope():
                client.is_enabled(flag_key)
                # use a tag to identify to identify events later on
                sentry_sdk.set_tag("task_id", flag_key)
                sentry_sdk.capture_exception(Exception("something wrong!"))

        # Capture an eval before we split isolation scopes.
        client.is_enabled("hello")

        with cf.ThreadPoolExecutor(max_workers=2) as pool:
            pool.map(task, ["world", "other"])

    # Capture error in original scope
    sentry_sdk.set_tag("task_id", "0")
    sentry_sdk.capture_exception(Exception("something wrong!"))

    assert len(events) == 3
    events.sort(key=lambda e: e["tags"]["task_id"])

    assert events[0]["contexts"]["flags"] == {
        "values": [
            {"flag": "hello", "result": True},
        ]
    }
    assert events[1]["contexts"]["flags"] == {
        "values": [
            {"flag": "hello", "result": True},
            {"flag": "other", "result": False},
        ]
    }
    assert events[2]["contexts"]["flags"] == {
        "values": [
            {"flag": "hello", "result": True},
            {"flag": "world", "result": False},
        ]
    }


@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_is_enabled_asyncio(sentry_init, capture_events, uninstall_integration):
    asyncio = pytest.importorskip("asyncio")
    uninstall_integration(UnleashIntegration.identifier)

    with mock_unleash_client():
        client = UnleashClient()  # type: ignore[arg-type]
        sentry_init(integrations=[UnleashIntegration()])
        events = capture_events()

        async def task(flag_key):
            with sentry_sdk.isolation_scope():
                client.is_enabled(flag_key)
                # use a tag to identify to identify events later on
                sentry_sdk.set_tag("task_id", flag_key)
                sentry_sdk.capture_exception(Exception("something wrong!"))

        async def runner():
            return asyncio.gather(task("world"), task("other"))

        # Capture an eval before we split isolation scopes.
        client.is_enabled("hello")

        asyncio.run(runner())

    # Capture error in original scope
    sentry_sdk.set_tag("task_id", "0")
    sentry_sdk.capture_exception(Exception("something wrong!"))

    assert len(events) == 3
    events.sort(key=lambda e: e["tags"]["task_id"])

    assert events[0]["contexts"]["flags"] == {
        "values": [
            {"flag": "hello", "result": True},
        ]
    }
    assert events[1]["contexts"]["flags"] == {
        "values": [
            {"flag": "hello", "result": True},
            {"flag": "other", "result": False},
        ]
    }
    assert events[2]["contexts"]["flags"] == {
        "values": [
            {"flag": "hello", "result": True},
            {"flag": "world", "result": False},
        ]
    }


def test_wraps_original(sentry_init, uninstall_integration):
    with mock_unleash_client():
        client = UnleashClient()  # type: ignore[arg-type]

        mock_is_enabled = mock.Mock(return_value=random() < 0.5)
        client.is_enabled = mock_is_enabled

        uninstall_integration(UnleashIntegration.identifier)
        sentry_init(integrations=[UnleashIntegration()])  # type: ignore

    res = client.is_enabled("test-flag", "arg", kwarg=1)
    assert res == mock_is_enabled.return_value
    assert mock_is_enabled.call_args == (
        ("test-flag", "arg"),
        {"kwarg": 1},
    )


def test_wrapper_attributes(sentry_init, uninstall_integration):
    with mock_unleash_client():
        client = UnleashClient()  # type: ignore[arg-type]

        original_is_enabled = client.is_enabled

        uninstall_integration(UnleashIntegration.identifier)
        sentry_init(integrations=[UnleashIntegration()])  # type: ignore

        # Mock clients methods have not lost their qualified names after decoration.
        assert client.is_enabled.__name__ == "is_enabled"
        assert client.is_enabled.__qualname__ == original_is_enabled.__qualname__


@pytest.mark.parametrize(
    "span_streaming",
    [True, False],
)
def test_unleash_span_integration(
    sentry_init, capture_events, capture_items, uninstall_integration, span_streaming
):
    uninstall_integration(UnleashIntegration.identifier)

    with mock_unleash_client():
        sentry_init(
            traces_sample_rate=1.0,
            integrations=[UnleashIntegration()],
            trace_lifecycle="stream" if span_streaming else "static",
        )

        client = UnleashClient()  # type: ignore[arg-type]

        if span_streaming:
            items = capture_items("span")
            with sentry_sdk.traces.start_span(name="bar"):
                client.is_enabled("hello")
                client.is_enabled("other")

            sentry_sdk.flush()

            assert len(items) == 1
            span = items[0].payload
            assert span["attributes"]["flag.evaluation.hello"] is True
            assert span["attributes"]["flag.evaluation.other"] is False

        else:
            events = capture_events()
            with start_transaction(name="hi"):
                with start_span(op="foo", name="bar"):
                    client.is_enabled("hello")
                    client.is_enabled("other")

            (event,) = events
            assert event["spans"][0]["data"] == ApproxDict(
                {"flag.evaluation.hello": True, "flag.evaluation.other": False}
            )
