"""
Integration tests for the native crash backend.

Tests crash handling, minidump generation, Build ID/UUID extraction,
multi-thread capture, and FPU/SIMD register capture on all platforms.
"""

import os
import sys
import time
import struct
import pytest

from . import (
    is_feedback_envelope,
    make_dsn,
    run,
    Envelope,
    split_log_request_cond,
)
from .assertions import (
    assert_breadcrumb,
    assert_meta,
    assert_session,
    wait_for_file,
    assert_user_feedback,
)
from .conditions import has_native, has_oom, is_kcov, is_asan, is_tsan, is_qemu

pytestmark = pytest.mark.skipif(
    not has_native or is_qemu,
    reason="Tests need the native backend enabled",
)

# Sanitizer builds are slower, so selected native crash tests use the same 10s
# timeout the native daemon used before it respected the SDK shutdown timeout.
SANITIZER_ARGS = ["shutdown-timeout", "10000"] if is_asan or is_tsan else []


def run_crash(tmp_path, exe, args, env):
    """
    Run a crash test, handling kcov's quirk of exiting with 0.
    kcov intercepts signals and may exit cleanly even when the program crashes.

    When running under ASAN, we configure it to not intercept crash signals
    so that our native crash handler can run and capture the crash.
    """
    # When running under ASAN, disable ASAN's signal handling so our crash
    # handler can run. ASAN would otherwise intercept SIGSEGV/SIGABRT/etc
    # and terminate the process before our handler completes.
    if is_asan:
        # Preserve existing ASAN_OPTIONS and add signal handling overrides
        asan_opts = env.get("ASAN_OPTIONS", "")
        # Disable handling of crash signals so our handler can run
        asan_signal_opts = (
            "handle_segv=0:handle_sigbus=0:handle_abort=0:"
            "handle_sigfpe=0:handle_sigill=0:allow_user_segv_handler=1"
        )
        if asan_opts:
            env["ASAN_OPTIONS"] = f"{asan_opts}:{asan_signal_opts}"
        else:
            env["ASAN_OPTIONS"] = asan_signal_opts

    if is_kcov:
        try:
            run(tmp_path, exe, args, expect_failure=True, env=env)
        except AssertionError:
            # kcov may exit with 0 even on crash, that's acceptable
            pass
    else:
        run(tmp_path, exe, args, expect_failure=True, env=env)


def test_native_capture_crash(cmake, httpserver):
    """Test basic crash capture with native backend"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "test-logger", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result


@pytest.mark.skipif(not has_oom, reason="OOM test unreliable in this environment")
def test_native_oom(cmake, httpserver):
    """Test OOM crash capture with native backend"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "oom"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result


def test_native_capture_minidump_generated(cmake, httpserver):
    """Test that minidump file is generated"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Crash the app - we verify crash by checking minidump generation below
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "test-logger", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result

    # Check for minidump file in database directory
    db_dir = tmp_path / ".sentry-native"
    assert db_dir.exists()

    assert wait_for_file(db_dir / "*.dmp"), "Minidump file should be generated"
    minidump_files = list(db_dir.glob("*.dmp"))

    # Verify minidump has correct header
    minidump_path = minidump_files[0]
    with open(minidump_path, "rb") as f:
        # Read minidump signature (should be MDMP = 0x504d444d)
        signature = struct.unpack("<I", f.read(4))[0]
        assert signature == 0x504D444D, "Minidump should have correct signature"

        # Read version
        version = struct.unpack("<I", f.read(4))[0]
        # On Windows, MiniDumpWriteDump uses system version format (0xa0f4a793)
        # On Unix, we use custom format with version 0xa793
        if sys.platform != "win32":
            assert version == 0xA793, "Minidump should have correct version"
        else:
            # Windows minidumps have a different version format
            # Just verify it's non-zero
            assert version != 0, "Minidump should have non-zero version"


def test_native_breadcrumbs(cmake, httpserver):
    """Test that breadcrumbs are captured before crash"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Add breadcrumbs then crash (use stdout for initialization delay under sanitizers)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "breadcrumb-log", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result

    # Verify breadcrumbs in envelope
    assert len(httpserver.log) >= 1
    envelope = Envelope.deserialize(httpserver.log[0][0].get_data())
    assert envelope.get_event()


def test_native_session_tracking(cmake, httpserver):
    """Test that sessions are tracked correctly with crashes"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Start session and crash (use stdout to add initialization delay for TSAN)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "start-session", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result

    # Check for session data - sent as a separate envelope from the run folder
    has_session = False
    for req in httpserver.log:
        data = req[0].get_data()
        if b'"type":"session"' in data or b'"status":"crashed"' in data:
            has_session = True
            break

    assert has_session, "Should have session data (standalone or embedded)"


def test_native_signal_handling(cmake, httpserver):
    """Test that different signals are handled correctly"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Test SIGSEGV (use stdout to add initialization delay for TSAN)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result


@pytest.mark.skipif(sys.platform == "win32", reason="POSIX signals only")
def test_native_sigabrt(cmake, httpserver):
    """Test SIGABRT handling"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Trigger SIGABRT via assert (use stdout for initialization delay under TSAN)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "assert"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result


def test_native_multiple_crashes(cmake, httpserver):
    """Test handling multiple crashes in sequence"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Crash multiple times (use stdout for initialization delay under TSAN)
    with httpserver.wait(timeout=10) as waiting:
        for i in range(3):
            run_crash(
                tmp_path,
                "sentry_example",
                ["log", "stdout", "crash"] + SANITIZER_ARGS,
                env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
            )
    assert waiting.result


def test_native_context_capture(cmake, httpserver):
    """Test that scope and context are captured"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Set context then crash (use log and stdout for initialization delay under TSAN)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "add-stacktrace", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result


def test_native_daemon_respawn(cmake, httpserver):
    """Test that daemon respawns if it dies"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # This tests the fallback mechanism if daemon dies
    # The test is platform-specific and may need adjustment
    # Use stdout for initialization delay under TSAN
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result


@pytest.mark.skipif(
    sys.platform not in ["linux", "darwin"],
    reason="Multi-thread test for POSIX platforms",
)
def test_native_multithreaded_crash(cmake, httpserver):
    """Test crash from non-main thread"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Crash from thread (use stdout for initialization delay under TSAN)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "crash"] + SANITIZER_ARGS,
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result


def test_native_minidump_streams(cmake, httpserver):
    """Test that minidump contains required streams"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Crash (use stdout for initialization delay under TSAN)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result

    # Find minidump
    db_dir = tmp_path / ".sentry-native"
    assert wait_for_file(db_dir / "*.dmp")
    minidump_files = list(db_dir.glob("*.dmp"))
    assert len(minidump_files) > 0

    # Parse minidump header and verify streams
    with open(minidump_files[0], "rb") as f:
        # Skip signature and version
        f.seek(8)

        # Read stream count
        stream_count = struct.unpack("<I", f.read(4))[0]
        assert (
            stream_count >= 3
        ), "Should have at least SystemInfo, ThreadList, ModuleList"

        # Read stream directory RVA
        stream_dir_rva = struct.unpack("<I", f.read(4))[0]

        # Seek to stream directory
        f.seek(stream_dir_rva)

        # Read stream types
        stream_types = []
        for _ in range(stream_count):
            stream_type = struct.unpack("<I", f.read(4))[0]
            stream_types.append(stream_type)
            f.read(8)  # Skip size and RVA

        # Verify required streams
        # 3 = SystemInfo, 4 = ThreadList, 5 = ModuleList
        assert 3 in stream_types, "Should have SystemInfo stream"
        assert 4 in stream_types, "Should have ThreadList stream"
        assert 5 in stream_types, "Should have ModuleList stream"


def test_native_cleanup(cmake):
    """Test that cleanup works properly"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    # Run and exit cleanly
    run(
        tmp_path,
        "sentry_example",
        ["log", "no-setup"],
    )

    # Database should exist
    db_dir = tmp_path / ".sentry-native"
    assert db_dir.exists()


def test_native_no_dsn_no_crash(cmake):
    """Test that without DSN, crashes don't create files"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})

    # Run without DSN (use stdout for initialization delay under TSAN)
    run_crash(
        tmp_path,
        "sentry_example",
        ["log", "stdout", "crash"],
        env=dict(os.environ, SENTRY_DSN=""),
    )

    # Should not create database
    db_dir = tmp_path / ".sentry-native"
    if db_dir.exists():
        minidump_files = list(db_dir.glob("*.dmp"))
        # Minidumps might still be generated for debugging
        # but won't be uploaded


def test_native_external_crash_reporter(cmake, httpserver):
    """Test external crash reporter invocation with native backend"""
    tmp_path = cmake(
        ["sentry_example", "sentry_crash_reporter"], {"SENTRY_BACKEND": "native"}
    )

    env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Crash and use external reporter
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "crash-reporter", "crash"],
            env=env,
        )
    assert waiting.result

    # Should have sent crash report and feedback via external reporter
    assert len(httpserver.log) == 2
    feedback_request, crash_request = split_log_request_cond(
        httpserver.log, is_feedback_envelope
    )
    feedback = feedback_request.get_data()
    crash = crash_request.get_data()

    # Verify it's a minidump crash report and user feedback
    envelope = Envelope.deserialize(crash)
    assert_meta(envelope)
    assert_breadcrumb(envelope)

    envelope = Envelope.deserialize(feedback)
    assert_user_feedback(envelope)


def test_crash_mode_minidump_only(cmake, httpserver):
    """Mode 1: Should produce envelope with minidump attachment only"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Crash with mode 1 (minidump only)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "crash-mode", "minidump", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result

    assert len(httpserver.log) >= 1
    envelope = Envelope.deserialize(httpserver.log[0][0].get_data())

    # Should have minidump attachment
    has_minidump = any(
        item.headers.get("type") == "attachment"
        and item.headers.get("attachment_type") == "event.minidump"
        for item in envelope.items
    )
    assert has_minidump, "Minidump mode should include minidump"


def test_crash_mode_native_only(cmake, httpserver):
    """Mode 2: Should produce envelope with native stacktrace, no minidump"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Crash with mode 2 (native only)
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "crash-mode", "native", "crash"],
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result

    assert len(httpserver.log) >= 1
    envelope = Envelope.deserialize(httpserver.log[0][0].get_data())

    # Should NOT have minidump
    has_minidump = any(
        item.headers.get("type") == "attachment"
        and item.headers.get("attachment_type") == "event.minidump"
        for item in envelope.items
    )
    assert not has_minidump, "Native mode should NOT include minidump"

    # Should have native stacktrace
    event = envelope.get_event()
    assert event is not None
    assert "exception" in event
    exc = event["exception"]["values"][0]
    assert exc["mechanism"]["type"] == "signalhandler"
    assert "stacktrace" in exc
    assert len(exc["stacktrace"]["frames"]) > 0

    # Each frame should have instruction_addr
    for frame in exc["stacktrace"]["frames"]:
        assert "instruction_addr" in frame

    if sys.platform == "win32":
        # At least some frames should have symbolicated function names
        assert any(
            frame.get("function") is not None for frame in exc["stacktrace"]["frames"]
        )

    # Should have debug_meta
    assert "debug_meta" in event
    assert len(event["debug_meta"]["images"]) > 0


def test_crash_mode_native_with_minidump(cmake, httpserver):
    """Mode 3 (default): Should have both native stacktrace AND minidump"""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")

    # Default mode should be NATIVE_WITH_MINIDUMP
    with httpserver.wait(timeout=10) as waiting:
        run_crash(
            tmp_path,
            "sentry_example",
            ["log", "stdout", "crash"],  # No crash-mode arg = use default
            env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)),
        )
    assert waiting.result

    assert len(httpserver.log) >= 1
    envelope = Envelope.deserialize(httpserver.log[0][0].get_data())

    # Should have BOTH minidump attachment
    has_minidump = any(
        item.headers.get("type") == "attachment"
        and item.headers.get("attachment_type") == "event.minidump"
        for item in envelope.items
    )
    assert has_minidump, "Native with minidump mode should include minidump"

    # AND native stacktrace
    event = envelope.get_event()
    assert event is not None
    assert "exception" in event
    exc = event["exception"]["values"][0]
    assert exc["mechanism"]["type"] == "signalhandler"
    assert "stacktrace" in exc
    assert len(exc["stacktrace"]["frames"]) > 0
    if sys.platform == "win32":
        # At least some frames should have symbolicated function names
        assert any(
            frame.get("function") is not None for frame in exc["stacktrace"]["frames"]
        )

    # Should have debug_meta
    assert "debug_meta" in event


def test_native_cache_consent(cmake, httpserver):
    """Daemon honors revoked consent: envelope is cached, not sent. Giving
    consent in a subsequent run flushes the cached envelope."""
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})
    cache_dir = tmp_path / ".sentry-native" / "cache"
    env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))

    # 1) Crash while consent is revoked. Envelope is cached, no upload.
    run_crash(
        tmp_path,
        "sentry_example",
        [
            "log",
            "stdout",
            "cache-keep",
            "http-retry",
            "require-user-consent",
            "user-consent-revoke",
            "crash",
        ],
        env=env,
    )

    assert wait_for_file(cache_dir / "*.envelope")
    assert len(list(cache_dir.glob("*.envelope"))) == 1
    assert len(httpserver.log) == 0

    # 2) Give consent. The cached envelope should be flushed to the server.
    httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK")
    with httpserver.wait(timeout=10) as waiting:
        run(
            tmp_path,
            "sentry_example",
            [
                "log",
                "cache-keep",
                "http-retry",
                "require-user-consent",
                "user-consent-give",
            ],
            env=env,
        )
    assert waiting.result
    assert len(list(cache_dir.glob("*.envelope"))) == 0


@pytest.mark.parametrize("cache_keep", [True, False])
def test_native_cache_keep(cmake, cache_keep, unreachable_dsn):
    tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"})
    db_dir = tmp_path / ".sentry-native"
    cache_dir = db_dir / "cache"
    env = dict(os.environ, SENTRY_DSN=unreachable_dsn)

    # crash -> daemon sends via HTTP -> unreachable -> cache
    run_crash(
        tmp_path,
        "sentry_example",
        ["log", "stdout", "crash"] + (["cache-keep"] if cache_keep else []),
        env=env,
    )

    if cache_keep:
        assert wait_for_file(cache_dir / "*.envelope")
        cache_files = list(cache_dir.glob("*.envelope"))
        assert len(cache_files) == 1
        dmp_files = list(cache_dir.glob("*.dmp"))
        assert len(dmp_files) == 1
        assert cache_files[0].stem == dmp_files[0].stem
    else:
        # Best-effort wait for crash processing to finish. 2s is not
        # guaranteed to be enough, but we cannot poll for the non-existence
        # of a file.
        time.sleep(2)
        assert len(list(cache_dir.glob("*.envelope"))) == 0
