"""
Test for Bug #1: ML Confidence Bypass Exploit

This test verifies that dangerous functions (os.system, subprocess, etc.) are ALWAYS
detected as CRITICAL severity, even when ML confidence is 100%.

Bug Description:
- Attackers could inject many torch module references to boost ML confidence to 100%
- This triggered severity downgrade: os.system CRITICAL → WARNING
- Wildcards in ML_SAFE_GLOBALS allowed bypassing security checks

Fix:
- Added ALWAYS_DANGEROUS_FUNCTIONS/MODULES lists
- These are NEVER whitelisted regardless of ML confidence
- Removed severity downgrade for always-dangerous functions
"""

import logging
from pathlib import Path

import pytest

from modelaudit.scanners.base import IssueSeverity
from modelaudit.scanners.pickle_scanner import PickleScanner

logger = logging.getLogger(__name__)

# Test data directory
EXPLOITS_DIR = Path(__file__).parent / "assets" / "exploits"


@pytest.mark.unit
@pytest.mark.parametrize(
    "exploit_file,description",
    [
        ("exploit1_basic_torch_bypass.pkl", "Basic torch bypass attempt (low confidence)"),
        ("exploit2_advanced_torch_bypass.pkl", "Advanced torch bypass"),
        ("exploit3_sophisticated_hybrid.pkl", "Sophisticated hybrid attack"),
        ("exploit4_supply_chain_attack.pkl", "Supply chain attack simulation"),
        ("exploit5_ultra_high_confidence.pkl", "Ultra high confidence exploit"),
        ("exploit6_ordereddict_bypass.pkl", "OrderedDict bypass attempt"),
        ("exploit7_nested_collections.pkl", "Nested collections exploit"),
        ("exploit9_manual_construction.pkl", "Manually constructed exploit"),
        ("exploit_ultimate_50pct.pkl", "Ultimate exploit with 100% ML confidence"),
    ],
)
def test_exploit_files_detect_dangerous_functions(exploit_file: str, description: str) -> None:
    """
    Test that all exploit files correctly identify dangerous functions as CRITICAL,
    regardless of ML confidence level.

    The key exploit file is exploit_ultimate_50pct.pkl which has 100% ML confidence
    but should still flag os.system as CRITICAL (not downgraded to WARNING).
    """
    file_path = EXPLOITS_DIR / exploit_file

    if not file_path.exists():
        pytest.skip(f"Exploit file not found: {exploit_file}")

    scanner = PickleScanner()
    result = scanner.scan(str(file_path))

    # Should have issues (dangerous functions detected)
    assert len(result.issues) > 0, f"{description}: No issues detected"

    # Check for dangerous function references (os.system, subprocess, etc.)
    # Only match issues that are explicitly flagged as "Suspicious reference" or "Suspicious module reference"
    # to avoid false positives from words like "execution" containing "exec"
    dangerous_refs = [
        issue
        for issue in result.issues
        if "suspicious reference" in issue.message.lower() or "suspicious module reference" in issue.message.lower()
    ]

    assert len(dangerous_refs) > 0, f"{description}: No dangerous function references detected"

    # CRITICAL ASSERTION: Dangerous functions should ALWAYS be CRITICAL severity
    # even with 100% ML confidence (bug1 fix verification)
    for issue in dangerous_refs:
        assert issue.severity == IssueSeverity.CRITICAL, (
            f"{description}: Dangerous function '{issue.message}' has severity {issue.severity}, "
            f"expected CRITICAL. This indicates the ML confidence bypass bug is not fixed!"
        )


@pytest.mark.unit
def test_ultimate_exploit_high_ml_confidence_still_critical() -> None:
    """
    Specific test for the most sophisticated exploit (100% ML confidence).

    This is the exploit that successfully bypassed detection in the bug report.
    With the fix, it should STILL detect os.system as CRITICAL.
    """
    exploit_file = EXPLOITS_DIR / "exploit_ultimate_50pct.pkl"

    if not exploit_file.exists():
        pytest.skip("Ultimate exploit file not found")

    scanner = PickleScanner()
    result = scanner.scan(str(exploit_file))

    # Verify high ML confidence (this is what the exploit achieves)
    ml_context = result.metadata.get("ml_context", {})
    confidence = ml_context.get("overall_confidence", 0)

    # The exploit should achieve high confidence (that's the attack)
    # But the dangerous functions should STILL be CRITICAL
    logger.debug(f"ML Confidence: {confidence:.2f}")
    logger.debug(f"Issues found: {len(result.issues)}")

    # Find os.system references
    os_system_issues = [
        issue
        for issue in result.issues
        if "os.system" in issue.message.lower() or "system" in issue.details.get("function", "").lower()
    ]

    assert len(os_system_issues) > 0, "os.system reference not detected in ultimate exploit"

    # CRITICAL: Even with 100% ML confidence, os.system must be CRITICAL
    for issue in os_system_issues:
        assert issue.severity == IssueSeverity.CRITICAL, (
            f"BUG1 NOT FIXED: os.system has severity {issue.severity} with ML confidence {confidence:.2f}. "
            f"Expected CRITICAL. The ML confidence bypass vulnerability is still present!"
        )

    logger.debug(
        f"✓ Bug1 fix verified: os.system correctly detected as CRITICAL despite {confidence:.2f} ML confidence"
    )


@pytest.mark.unit
def test_always_dangerous_functions_never_whitelisted() -> None:
    """
    Test that functions in ALWAYS_DANGEROUS_FUNCTIONS are never considered safe,
    regardless of module or ML confidence.
    """
    from modelaudit.scanners.pickle_scanner import ALWAYS_DANGEROUS_FUNCTIONS, ALWAYS_DANGEROUS_MODULES

    # Verify the fix is in place: ALWAYS_DANGEROUS lists exist
    assert len(ALWAYS_DANGEROUS_FUNCTIONS) > 0, "ALWAYS_DANGEROUS_FUNCTIONS list is empty - bug1 fix not applied"
    assert len(ALWAYS_DANGEROUS_MODULES) > 0, "ALWAYS_DANGEROUS_MODULES list is empty - bug1 fix not applied"

    # Key dangerous functions that should ALWAYS be flagged
    required_dangerous_functions = {
        "os.system",
        "subprocess.call",
        "subprocess.Popen",
        "eval",
        "exec",
        "compile",
        "__import__",
    }

    missing = required_dangerous_functions - ALWAYS_DANGEROUS_FUNCTIONS
    assert len(missing) == 0, f"Missing required dangerous functions in ALWAYS_DANGEROUS_FUNCTIONS: {missing}"

    # Key dangerous modules that should ALWAYS be flagged
    required_dangerous_modules = {"os", "subprocess", "sys", "posix", "nt"}

    missing_modules = required_dangerous_modules - ALWAYS_DANGEROUS_MODULES
    assert len(missing_modules) == 0, (
        f"Missing required dangerous modules in ALWAYS_DANGEROUS_MODULES: {missing_modules}"
    )

    logger.debug(f"✓ ALWAYS_DANGEROUS_FUNCTIONS contains {len(ALWAYS_DANGEROUS_FUNCTIONS)} functions")
    logger.debug(f"✓ ALWAYS_DANGEROUS_MODULES contains {len(ALWAYS_DANGEROUS_MODULES)} modules")
