import logging
import os
import subprocess
import time

import pytest

from ddtrace.appsec._common_module_patches import patch_common_modules
from ddtrace.appsec._common_module_patches import unpatch_common_modules
from ddtrace.appsec._constants import IAST
from ddtrace.appsec._iast._overhead_control_engine import oce
from ddtrace.appsec._iast._patch_modules import _testing_unpatch_iast
from ddtrace.appsec._iast._patches.json_tainting import patch as json_patch
from ddtrace.appsec._iast._taint_tracking._context import debug_context_array_free_slots_number
from ddtrace.appsec._iast._taint_tracking._context import debug_context_array_size
from ddtrace.appsec._iast.taint_sinks.code_injection import patch as code_injection_patch
from ddtrace.appsec._iast.taint_sinks.header_injection import patch as header_injection_patch
from ddtrace.appsec._iast.taint_sinks.untrusted_serialization import patch as unstrusted_serialization_patch
from ddtrace.appsec._iast.taint_sinks.weak_cipher import patch as weak_cipher_patch
from ddtrace.appsec._iast.taint_sinks.weak_hash import patch as weak_hash_patch
from ddtrace.appsec._iast.taint_sinks.weak_hash import unpatch_iast as weak_hash_unpatch
from ddtrace.internal.utils.http import Response
from ddtrace.internal.utils.http import get_connection
from tests.appsec.iast.iast_utils import IAST_VALID_LOG
from tests.appsec.iast.iast_utils import _end_iast_context_and_oce
from tests.appsec.iast.iast_utils import _start_iast_context_and_oce
from tests.utils import override_env
from tests.utils import override_global_config


CONFIG_SERVER_PORT = "9596"


@pytest.fixture
def no_request_sampling(tracer):
    with override_env(
        {
            IAST.ENV_REQUEST_SAMPLING: "100",
            "DD_IAST_MAX_CONCURRENT_REQUEST": "100",
        }
    ):
        oce.reconfigure()
        yield


def iast_context(env, request_sampling=100.0, deduplication=False, asm_enabled=False, vulnerabilities_per_requests=100):
    class MockSpan:
        _trace_id_64bits = 17577308072598193742

    env.update({"DD_IAST_DEDUPLICATION_ENABLED": str(deduplication)})
    # env.update({"DD_IAST_MAX_CONCURRENT_REQUESTS": "100"})
    with override_global_config(
        dict(
            _asm_enabled=asm_enabled,
            _iast_enabled=True,
            _iast_is_testing=True,
            _iast_deduplication_enabled=deduplication,
            _iast_max_vulnerabilities_per_requests=vulnerabilities_per_requests,
            _iast_request_sampling=request_sampling,
        )
    ), override_env(env):
        assert debug_context_array_size() == 2
        assert debug_context_array_free_slots_number() > 0
        span = MockSpan()
        _start_iast_context_and_oce(span)
        weak_hash_patch()
        weak_cipher_patch()
        unstrusted_serialization_patch()
        json_patch()
        header_injection_patch()
        code_injection_patch()
        patch_common_modules()
        try:
            yield
        finally:
            unpatch_common_modules()
            weak_hash_unpatch()
            _testing_unpatch_iast()
            _end_iast_context_and_oce(span)


@pytest.fixture
def iast_context_defaults():
    yield from iast_context(dict(DD_IAST_ENABLED="true"))


@pytest.fixture
def iast_context_deduplication_enabled(tracer):
    yield from iast_context(
        dict(DD_IAST_ENABLED="true", DD_IAST_REQUEST_SAMPLING="100.0"),
        deduplication=True,
        vulnerabilities_per_requests=2,
    )


@pytest.fixture
def iast_context_2_vulnerabilities_per_requests(tracer):
    yield from iast_context(
        dict(DD_IAST_ENABLED="true", DD_IAST_REQUEST_SAMPLING="100.0"), vulnerabilities_per_requests=2
    )


@pytest.fixture
def iast_span_defaults(tracer):
    for _ in iast_context(dict(DD_IAST_ENABLED="true")):
        with tracer.trace("test") as span:
            yield span


def pytest_configure(config):
    config.addinivalue_line(
        "markers",
        "skip_iast_check_logs: mark test to remove _DD_IAST_DEBUG environment variable and skip logs checks to validate"
        "if the propagation is not running outside the context",
    )


@pytest.fixture(autouse=True)
def check_native_code_exception_in_each_python_aspect_test(request, caplog):
    if "skip_iast_check_logs" in request.keywords:
        yield
    else:
        with override_global_config(dict(_iast_debug=True)), caplog.at_level(logging.DEBUG):
            yield

        for record in caplog.get_records("call"):
            if hasattr(record, "message") and IAST_VALID_LOG.search(record.message):
                import traceback

                formatted_trace = "".join(traceback.format_exception(*record.exc_info))
                pytest.fail(f"{record.message}:\n{formatted_trace}")


@pytest.fixture(scope="session")
def configuration_endpoint():
    current_dir = os.path.dirname(__file__)
    status = None
    retries = 0
    while status != 200 and retries < 5:
        cmd = [
            "python",
            os.path.join(current_dir, "fixtures", "integration", "http_config_server.py"),
            CONFIG_SERVER_PORT,
        ]
        try:
            process = subprocess.Popen(cmd, cwd=current_dir)
            time.sleep(0.9)

            url = f"http://localhost:{CONFIG_SERVER_PORT}/"
            conn = get_connection(url)

            conn.request("GET", "/")
            response = conn.getresponse()
            result = Response.from_http_response(response)
            status = result.status
        except (OSError, ConnectionError, PermissionError):
            pass
        retries += 1

    if retries == 5:
        pytest.skip("Failed to start the configuration server")

    yield
    process.kill()


@pytest.fixture(autouse=True)
def clear_iast_env_vars():
    os.environ[IAST.PATCH_MODULES] = "benchmarks.,tests.appsec."
    if IAST.DENY_MODULES in os.environ:
        os.environ.pop("_DD_IAST_DENY_MODULES")
    yield
