"""Tests for ELF-only mode ABI-relevance symbol filter.

Covers _is_abi_relevant_symbol() which strips compiler internals, transitive
stdlib symbols, and private double-underscore names from .dynsym before they
are exposed as "public ABI" in ELF-only mode.
"""

from __future__ import annotations

import pytest

from abicheck.dumper import _is_abi_relevant_symbol
from abicheck.dwarf_snapshot import _DwarfSnapshotBuilder
from abicheck.elf_metadata import ElfMetadata, ElfSymbol, SymbolType
from abicheck.elf_symbol_filter import (
    FUNCTION_SYMBOL_TYPES,
    VARIABLE_SYMBOL_TYPES,
    exported_symbol_names,
)

# ---------------------------------------------------------------------------
# Bug 1: GCC / compiler-internal symbols — must be filtered
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("name", [
    # ix86 tune/arch internals (mpfr 4.2.0→4.2.1 false positives)
    "ix86_tune_indices",
    "ix86_arch_mask",
    "ix86_cost_pointer",
    # x86_64 compiler internals
    "x86_64_cpu_info",
    # SIMD vector variants generated by GCC auto-vectoriser
    "_ZGVbN2v_sin",
    "_ZGVdN4v_exp",
    "_ZGVeN8v_log",
    # CPU model / features (runtime detection helpers)
    "__cpu_model",
    "__cpu_features2",
    "__cpu_features_init",
    # Intel SVML (Short Vector Math Library)
    "__svml_sin4",
    "__svml_exp2_mask",
    # libm SSE2 / AVX variants
    "__libm_sse2_sin",
    "__libm_avx_cos",
])
def test_gcc_internal_symbols_are_filtered(name: str) -> None:
    assert _is_abi_relevant_symbol(name) is False, (
        f"GCC internal symbol {name!r} should be filtered out"
    )


# ---------------------------------------------------------------------------
# Bug 2: Private double-underscore C symbols — must be filtered
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("name", [
    # HDF5 internal naming (hdf5 1.14.1→1.14.2 false positives)
    "H5C__flush_marked_entries",
    "H5C__mark_tagged_entries",
    "H5F__close",
    # MPI implementation internals
    "MPI__send",
    "PMPI__recv",
    # Generic library-private pattern
    "LibFoo__internal_helper",
    "mylib__priv_init",
])
def test_private_double_underscore_symbols_are_filtered(name: str) -> None:
    assert _is_abi_relevant_symbol(name) is False, (
        f"Private __-separator symbol {name!r} should be filtered out"
    )


# ---------------------------------------------------------------------------
# Bug 3: Transitive libstdc++ / libc++ symbols — must be filtered
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("name", [
    # libstdc++ std:: namespace (leveldb 1.22→1.23 false positives)
    "_ZNSt6thread8_M_startEv",
    "_ZNSt6vectorIiSaIiEE9push_backERKi",
    # libc++ inline namespace __1
    "_ZNSt3__16threadC1Ev",
    "_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6assignEPKc",
    # cv/ref-qualified std:: methods
    "_ZNKSt6vectorIiSaIiEE4sizeEv",
    "_ZNVSt6atomicIiEaSERKi",
    "_ZNRSt6atomicIiEaSERKi",
    "_ZNKRSt6atomicIiEcviv",
    "_ZNVRSt6atomicIiEaSERKi",
    # operator delete / new
    "_ZdlPv",
    "_ZdlPvSt11align_val_t",
    "_ZnwStmSt11align_val_t",
    "_ZnaSt12nothrow_tRKS_",
    "_ZdaPv",
    # RTTI vtables / typeinfo
    "_ZTVN10__cxxabiv117__class_type_infoE",
    "_ZTVN10__cxxabiv121__vmi_class_type_infoE",
    "_ZTISt9exception",
    "_ZTSSt9bad_alloc",
    # std:: global symbols
    "_ZSt4cout",
    "_ZSt4cerr",
])
def test_stdlib_transitive_symbols_are_filtered(name: str) -> None:
    assert _is_abi_relevant_symbol(name) is False, (
        f"Transitive stdlib symbol {name!r} should be filtered out"
    )


# ---------------------------------------------------------------------------
# Legitimate public-API symbols — must NOT be filtered
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("name", [
    # mpfr public API
    "mpfr_add",
    "mpfr_mul",
    "mpfr_set_prec",
    # HDF5 public API
    "H5Fopen",
    "H5Dread",
    "H5Screate_simple",
    # C++ mangled names from user library (NOT stdlib)
    "_ZN3Foo3barEv",
    "_ZN6MyLib4CoreC1Ev",
    "_ZNK6MyLib4Core4nameEv",
    "_ZTIN3foo3BarE",   # typeinfo for foo::Bar
    "_ZTSN3foo3BarE",   # typeinfo name for foo::Bar
    # Regular single-underscore prefixed symbols (not double)
    "_init",
    "_fini",
    # Symbol with single underscore separator — not private
    "my_function",
    "lib_init",
    # Empty-ish edge cases that are valid names
    "ab",          # too short for __ to appear after char[2]
    "a_b",         # single underscore, not double
])
def test_legitimate_symbols_are_not_filtered(name: str) -> None:
    assert _is_abi_relevant_symbol(name) is True, (
        f"Legitimate symbol {name!r} should NOT be filtered out"
    )


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------

def test_empty_string_is_filtered() -> None:
    assert _is_abi_relevant_symbol("") is False


def test_none_like_empty_is_filtered() -> None:
    # The function signature accepts str; passing "" covers the falsy branch.
    assert _is_abi_relevant_symbol("") is False


def test_short_name_without_double_underscore_passes() -> None:
    # "a__b": name[2:] = "_b" which does NOT contain "__" → the filter does
    # NOT trigger, so the symbol is considered ABI-relevant (returns True).
    # This confirms the boundary: __ must appear *within* name[2:], not straddle it.
    assert _is_abi_relevant_symbol("a__b") is True


def test_cxx_mangled_with_double_underscore_in_user_ns_passes() -> None:
    # A C++ mangled name starting with _Z is never touched by the __ filter
    # even if the demangled form contains __ (e.g. anonymous namespaces).
    name = "_ZN12_GLOBAL__N_15helperEv"  # anonymous namespace helper
    assert _is_abi_relevant_symbol(name) is True


@pytest.mark.parametrize("name,expected", [
    ("a__b", True),    # __ at index 1; name[2:] = "_b" — no "__" present
    ("ab__c", False),  # __ at index 2; name[2:] = "__c" — "__" present
    ("abc__d", False), # __ after index 2
])
def test_double_underscore_boundary(name: str, expected: bool) -> None:
    assert _is_abi_relevant_symbol(name) is expected, (
        f"_is_abi_relevant_symbol({name!r}) expected {expected}"
    )


def test_dwarf_export_index_uses_same_abi_relevance_filter(tmp_path) -> None:
    """DWARF mode must not re-admit stdlib weak exports as public functions."""
    meta = ElfMetadata(
        symbols=[
            ElfSymbol(name="_ZNSt10unique_ptrINSt6thread6_StateEEC1Ev"),
            ElfSymbol(name="_ZNVSt6atomicIiEaSERKi"),
            ElfSymbol(name="_ZN6MyLib4CoreC1Ev"),
        ]
    )
    builder = _DwarfSnapshotBuilder(tmp_path / "libfoo.so", meta)

    assert "_ZNSt10unique_ptrINSt6thread6_StateEEC1Ev" not in builder._exported_names
    assert "_ZNVSt6atomicIiEaSERKi" not in builder._exported_names
    assert "_ZN6MyLib4CoreC1Ev" in builder._exported_names
    assert builder._is_exported("_ZNSt10unique_ptrINSt6thread6_StateEEC1Ev", "std::unique_ptr") is False
    assert builder._is_exported("_ZNVSt6atomicIiEaSERKi", "std::atomic<int>::operator=") is False


def test_dwarf_export_index_preserves_runtime_owned_stdlib_exports(tmp_path) -> None:
    """libstdc++/libc++ own std:: symbols, so DWARF mode must not filter them."""
    meta = ElfMetadata(
        soname="libstdc++.so.6",
        symbols=[
            ElfSymbol(name="_ZNSt6vectorIiSaIiEE4sizeEv"),
            ElfSymbol(name="_ZSt4cout"),
        ],
    )
    builder = _DwarfSnapshotBuilder(tmp_path / "libstdc++.so.6", meta)

    assert "_ZNSt6vectorIiSaIiEE4sizeEv" in builder._exported_names
    assert "_ZSt4cout" in builder._exported_names


# ---------------------------------------------------------------------------
# Shared exported_symbol_names helper (used by diff_symbols / diff_types)
# ---------------------------------------------------------------------------


def test_exported_symbol_names_returns_empty_without_symbols() -> None:
    """No ELF table (None or empty) yields an empty set so callers fall back."""
    assert exported_symbol_names(None, FUNCTION_SYMBOL_TYPES) == set()
    assert exported_symbol_names(ElfMetadata(symbols=[]), FUNCTION_SYMBOL_TYPES) == set()


def test_exported_symbol_names_filters_by_symbol_type() -> None:
    """Only the requested STT_* types are returned; empty names are skipped."""
    meta = ElfMetadata(
        symbols=[
            ElfSymbol(name="do_work", sym_type=SymbolType.FUNC),
            ElfSymbol(name="resolver", sym_type=SymbolType.IFUNC),
            ElfSymbol(name="g_table", sym_type=SymbolType.OBJECT),
            ElfSymbol(name="", sym_type=SymbolType.FUNC),
        ]
    )
    assert exported_symbol_names(meta, FUNCTION_SYMBOL_TYPES) == {"do_work", "resolver"}
    assert exported_symbol_names(meta, VARIABLE_SYMBOL_TYPES) == {"g_table"}


def test_exported_symbol_names_notype_counts_for_both_surfaces() -> None:
    """STT_NOTYPE is intentionally accepted as both a func and a variable export."""
    meta = ElfMetadata(symbols=[ElfSymbol(name="stub", sym_type=SymbolType.NOTYPE)])
    assert "stub" in exported_symbol_names(meta, FUNCTION_SYMBOL_TYPES)
    assert "stub" in exported_symbol_names(meta, VARIABLE_SYMBOL_TYPES)


def test_exported_symbol_names_abi_relevant_only_drops_transitive_runtime() -> None:
    """abi_relevant_only excludes transitive stdlib exports but keeps own symbols."""
    meta = ElfMetadata(
        symbols=[
            ElfSymbol(name="_ZN6MyLib4CoreC1Ev", sym_type=SymbolType.FUNC),
            ElfSymbol(name="_ZNSt6vectorIiSaIiEE4sizeEv", sym_type=SymbolType.FUNC),
        ]
    )
    assert exported_symbol_names(meta, FUNCTION_SYMBOL_TYPES) == {
        "_ZN6MyLib4CoreC1Ev",
        "_ZNSt6vectorIiSaIiEE4sizeEv",
    }
    assert exported_symbol_names(meta, FUNCTION_SYMBOL_TYPES, abi_relevant_only=True) == {
        "_ZN6MyLib4CoreC1Ev",
    }
