"""
Unit tests for AdvancedPromptScanner internal helper methods.

The existing test_advanced_prompt_scanner.py covers scan_comprehensive()
end-to-end. This file targets the helper methods directly.
"""

from unittest.mock import patch

import pytest

from src.services.security.advanced_prompt_scanner import AdvancedPromptScanner, ThreatLevel


@pytest.fixture
def scanner():
    """Fresh scanner per test with Redis patched out to avoid cross-test state."""
    with patch("src.config.redis.get_redis", return_value=None):
        yield AdvancedPromptScanner()


# ---------------------------------------------------------------------------
# _normalize_text
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestNormalizeText:
    def test_strips_leading_trailing_whitespace(self, scanner):
        result = scanner._normalize_text("  hello world  ")
        assert result == "hello world"

    def test_collapses_multiple_spaces(self, scanner):
        result = scanner._normalize_text("hello   world")
        assert result == "hello world"

    def test_collapses_newlines_to_space(self, scanner):
        result = scanner._normalize_text("hello\n\nworld")
        assert result == "hello world"

    def test_lowercases_output(self, scanner):
        result = scanner._normalize_text("Hello World")
        assert result == "hello world"

    def test_empty_string_stays_empty(self, scanner):
        result = scanner._normalize_text("")
        assert result == ""


# ---------------------------------------------------------------------------
# _decode_obfuscations
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestDecodeObfuscations:
    def test_html_entity_decoded(self, scanner):
        result = scanner._decode_obfuscations("hello &amp; world")
        assert "&" in result

    def test_url_encoded_decoded(self, scanner):
        result = scanner._decode_obfuscations("hello%20world")
        assert " " in result

    def test_zero_width_chars_removed(self, scanner):
        # Insert a zero-width space
        text = "hel\u200blo"
        result = scanner._decode_obfuscations(text)
        assert "\u200b" not in result
        assert "hello" in result

    def test_normal_text_unchanged(self, scanner):
        result = scanner._decode_obfuscations("just normal text")
        assert result == "just normal text"


# ---------------------------------------------------------------------------
# _extract_context
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestExtractContext:
    def test_short_text_returns_full_text(self, scanner):
        text = "hello world"
        result = scanner._extract_context(text, 6, 11)
        assert "hello" in result
        assert "world" in result

    def test_adds_leading_ellipsis_when_truncated(self, scanner):
        text = "x" * 300
        result = scanner._extract_context(text, 200, 210)
        assert result.startswith("...")

    def test_adds_trailing_ellipsis_when_truncated(self, scanner):
        text = "x" * 300
        result = scanner._extract_context(text, 0, 10)
        assert result.endswith("...")

    def test_no_ellipsis_for_small_text(self, scanner):
        text = "short text here"
        result = scanner._extract_context(text, 6, 10)
        assert not result.startswith("...")
        assert not result.endswith("...")


# ---------------------------------------------------------------------------
# _get_risk_score
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGetRiskScore:
    def test_safe_severity_returns_zero(self, scanner):
        assert scanner._get_risk_score(ThreatLevel.SAFE, 1.0) == 0

    def test_low_severity_full_confidence(self, scanner):
        assert scanner._get_risk_score(ThreatLevel.LOW, 1.0) == 10

    def test_medium_severity_full_confidence(self, scanner):
        assert scanner._get_risk_score(ThreatLevel.MEDIUM, 1.0) == 25

    def test_high_severity_full_confidence(self, scanner):
        assert scanner._get_risk_score(ThreatLevel.HIGH, 1.0) == 50

    def test_critical_severity_full_confidence(self, scanner):
        assert scanner._get_risk_score(ThreatLevel.CRITICAL, 1.0) == 100

    def test_confidence_scales_score(self, scanner):
        assert scanner._get_risk_score(ThreatLevel.HIGH, 0.5) == 25

    def test_zero_confidence_returns_zero(self, scanner):
        assert scanner._get_risk_score(ThreatLevel.CRITICAL, 0.0) == 0


# ---------------------------------------------------------------------------
# _calculate_threat_level
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestCalculateThreatLevel:
    def test_zero_is_safe(self, scanner):
        assert scanner._calculate_threat_level(0) == ThreatLevel.SAFE

    def test_nine_is_safe(self, scanner):
        assert scanner._calculate_threat_level(9) == ThreatLevel.SAFE

    def test_ten_is_low(self, scanner):
        assert scanner._calculate_threat_level(10) == ThreatLevel.LOW

    def test_twenty_four_is_low(self, scanner):
        assert scanner._calculate_threat_level(24) == ThreatLevel.LOW

    def test_twenty_five_is_medium(self, scanner):
        assert scanner._calculate_threat_level(25) == ThreatLevel.MEDIUM

    def test_forty_nine_is_medium(self, scanner):
        assert scanner._calculate_threat_level(49) == ThreatLevel.MEDIUM

    def test_fifty_is_high(self, scanner):
        assert scanner._calculate_threat_level(50) == ThreatLevel.HIGH

    def test_ninety_nine_is_high(self, scanner):
        assert scanner._calculate_threat_level(99) == ThreatLevel.HIGH

    def test_one_hundred_is_critical(self, scanner):
        assert scanner._calculate_threat_level(100) == ThreatLevel.CRITICAL

    def test_above_hundred_is_critical(self, scanner):
        assert scanner._calculate_threat_level(200) == ThreatLevel.CRITICAL


# ---------------------------------------------------------------------------
# _analyze_reputation
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAnalyzeReputation:
    def test_unknown_user_scores_zero(self, scanner):
        score = scanner._analyze_reputation("new-user", None)
        assert score == 0

    def test_user_with_violations_scores_higher(self, scanner):
        scanner.reputation_cache["user_bad-actor"] = {"score": 0, "violations": 3}
        score = scanner._analyze_reputation("bad-actor", None)
        assert score == 30  # 3 × 10

    def test_ip_with_violations_scores_higher(self, scanner):
        scanner.reputation_cache["ip_1.2.3.4"] = {"score": 0, "violations": 4}
        score = scanner._analyze_reputation(None, "1.2.3.4")
        assert score == 20  # 4 × 5

    def test_reputation_capped_at_fifty(self, scanner):
        scanner.reputation_cache["user_spammer"] = {"score": 0, "violations": 100}
        score = scanner._analyze_reputation("spammer", None)
        assert score == 50

    def test_both_user_and_ip_combined(self, scanner):
        scanner.reputation_cache["user_u"] = {"score": 0, "violations": 2}
        scanner.reputation_cache["ip_10.0.0.1"] = {"score": 0, "violations": 2}
        score = scanner._analyze_reputation("u", "10.0.0.1")
        assert score == min(20 + 10, 50)  # 20 + 10 = 30, under cap


# ---------------------------------------------------------------------------
# _update_reputation
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestUpdateReputation:
    def test_high_threat_increments_user_violations(self, scanner):
        scanner._update_reputation("alice", None, ThreatLevel.HIGH)
        assert scanner.reputation_cache["user_alice"]["violations"] == 1

    def test_critical_threat_increments_ip_violations(self, scanner):
        scanner._update_reputation(None, "1.1.1.1", ThreatLevel.CRITICAL)
        assert scanner.reputation_cache["ip_1.1.1.1"]["violations"] == 1

    def test_low_threat_does_not_update_reputation(self, scanner):
        scanner._update_reputation("alice", "1.1.1.1", ThreatLevel.LOW)
        assert "user_alice" not in scanner.reputation_cache
        assert "ip_1.1.1.1" not in scanner.reputation_cache

    def test_safe_threat_does_not_update(self, scanner):
        scanner._update_reputation("bob", None, ThreatLevel.SAFE)
        assert "user_bob" not in scanner.reputation_cache

    def test_repeat_offences_accumulate(self, scanner):
        scanner._update_reputation("repeat", None, ThreatLevel.HIGH)
        scanner._update_reputation("repeat", None, ThreatLevel.CRITICAL)
        assert scanner.reputation_cache["user_repeat"]["violations"] == 2


# ---------------------------------------------------------------------------
# get_detection_stats
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGetDetectionStats:
    def test_empty_history_returns_zeros(self, scanner):
        stats = scanner.get_detection_stats()
        assert stats == {"total_scans": 0, "threats_detected": 0, "avg_risk_score": 0}

    def test_total_scans_reflects_history_size(self, scanner):
        scanner.detection_history = [{"risk_score": 10}, {"risk_score": 80}]
        stats = scanner.get_detection_stats()
        assert stats["total_scans"] == 2

    def test_threats_detected_counts_score_gte_25(self, scanner):
        scanner.detection_history = [
            {"risk_score": 24},  # not a threat
            {"risk_score": 25},  # threshold
            {"risk_score": 100},
        ]
        stats = scanner.get_detection_stats()
        assert stats["threats_detected"] == 2

    def test_avg_risk_score_correct(self, scanner):
        scanner.detection_history = [{"risk_score": 20}, {"risk_score": 60}]
        stats = scanner.get_detection_stats()
        assert stats["avg_risk_score"] == 40.0

    def test_threat_rate_calculated(self, scanner):
        scanner.detection_history = [{"risk_score": 100}, {"risk_score": 5}]
        stats = scanner.get_detection_stats()
        assert stats["threat_rate"] == 0.5


# ---------------------------------------------------------------------------
# _get_mitigation_advice
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGetMitigationAdvice:
    def test_known_category_returns_specific_advice(self, scanner):
        advice = scanner._get_mitigation_advice("jailbreak", ThreatLevel.CRITICAL)
        assert "jailbreak" in advice.lower() or "block" in advice.lower()

    def test_unknown_category_returns_generic_advice(self, scanner):
        advice = scanner._get_mitigation_advice("unknown_category", ThreatLevel.LOW)
        assert isinstance(advice, str)
        assert len(advice) > 0

    def test_system_injection_advice_mentions_critical(self, scanner):
        advice = scanner._get_mitigation_advice("system_injection", ThreatLevel.CRITICAL)
        assert "CRITICAL" in advice or "system" in advice.lower()
