"""
Tests for ChatFeatureExtractor and ChatFeatureVector.
=====================================================
Run with:  pytest tests/test_chat_features.py -v

All tests are pure-Python unit tests — no API keys or embedding models
required.  The cosine feature falls back to 0.5 when MiniLM is unavailable,
which is accounted for in the relevant assertions.
"""

import math
import pytest

from llm_guard.chat_features import ChatFeatureExtractor, ChatFeatureVector

# ---------------------------------------------------------------------------
# Shared extractor (stateless, safe to reuse across all tests)
# ---------------------------------------------------------------------------

extractor = ChatFeatureExtractor()


# ---------------------------------------------------------------------------
# Hedge density
# ---------------------------------------------------------------------------

class TestHedgeDensity:
    def test_hedge_density_high(self):
        resp = "I think probably this might be correct, I'm not sure though."
        vec = extractor.extract("q", resp)
        assert vec.hedge_density > 0.2

    def test_hedge_density_clean(self):
        resp = "The capital of France is Paris."
        vec = extractor.extract("q", resp)
        assert vec.hedge_density < 0.05


# ---------------------------------------------------------------------------
# Length ratio
# ---------------------------------------------------------------------------

class TestLengthRatio:
    def test_length_ratio_verbose(self):
        query = "Hi"
        resp = " ".join(["word"] * 200)
        vec = extractor.extract(query, resp)
        assert vec.length_ratio > 10

    def test_length_ratio_normal(self):
        vec = extractor.extract("What is Paris?", "Paris is the capital of France.")
        assert vec.length_ratio < 5


# ---------------------------------------------------------------------------
# First-person uncertainty
# ---------------------------------------------------------------------------

class TestFirstPersonUncertainty:
    def test_first_person_uncertainty_present(self):
        resp = "I don't know the exact date, I cannot confirm this."
        vec = extractor.extract("q", resp)
        assert vec.first_person_uncertainty > 0

    def test_first_person_uncertainty_absent(self):
        resp = "The Battle of Hastings was in 1066."
        vec = extractor.extract("q", resp)
        assert vec.first_person_uncertainty == 0


# ---------------------------------------------------------------------------
# Serialisation
# ---------------------------------------------------------------------------

class TestSerialisation:
    def test_to_array_length(self):
        vec = extractor.extract("query", "response text")
        arr = vec.to_array()
        assert len(arr) == 12

    def test_to_array_all_floats(self):
        vec = extractor.extract("query", "response text here")
        arr = vec.to_array()
        for val in arr:
            assert isinstance(val, float), f"Expected float, got {type(val)}: {val}"

    def test_to_dict_has_all_keys(self):
        vec = extractor.extract("q", "r")
        d = vec.to_dict()
        expected = {
            "hedge_density", "length_ratio", "first_person_uncertainty",
            "query_response_cosine", "entity_overlap_ratio", "conditional_density",
            "lexical_diversity", "response_word_count", "query_word_count",
            "uppercase_ratio", "numeric_density", "sentence_count",
        }
        assert set(d.keys()) == expected

    def test_to_dict_raw_values_match_fields(self):
        vec = extractor.extract("What year?", "It happened in 1969.")
        d = vec.to_dict()
        assert d["hedge_density"] == vec.hedge_density
        assert d["sentence_count"] == vec.sentence_count
        assert d["response_word_count"] == vec.response_word_count


# ---------------------------------------------------------------------------
# Normalisation in to_array()
# ---------------------------------------------------------------------------

class TestNormalisation:
    def test_length_ratio_capped_at_one(self):
        # 400 words vs 1-word query → raw ratio=400 → normalised min(400/20,1)=1.0
        query = "Hi"
        resp = " ".join(["word"] * 400)
        vec = extractor.extract(query, resp)
        arr = vec.to_array()
        assert arr[1] == 1.0

    def test_log_scaled_response_word_count(self):
        resp = "one two three"
        vec = extractor.extract("q", resp)
        arr = vec.to_array()
        expected = math.log1p(vec.response_word_count) / 7.0
        assert abs(arr[7] - expected) < 1e-9

    def test_log_scaled_sentence_count(self):
        resp = "Sentence one. Sentence two! Sentence three?"
        vec = extractor.extract("q", resp)
        arr = vec.to_array()
        expected = math.log1p(vec.sentence_count) / 4.0
        assert abs(arr[11] - expected) < 1e-9


# ---------------------------------------------------------------------------
# Entity overlap
# ---------------------------------------------------------------------------

class TestEntityOverlap:
    def test_entity_overlap_same_named_entity(self):
        vec = extractor.extract("Who is Albert Einstein?", "Albert Einstein was a physicist.")
        assert vec.entity_overlap_ratio > 0

    def test_entity_overlap_no_caps_in_response(self):
        vec = extractor.extract("q", "all lower case words here")
        assert vec.entity_overlap_ratio == 0.0


# ---------------------------------------------------------------------------
# Conditional density
# ---------------------------------------------------------------------------

class TestConditionalDensity:
    def test_conditional_density_high(self):
        resp = "If you might want to, you could try this. Would that work? If not, you may need help."
        vec = extractor.extract("q", resp)
        assert vec.conditional_density > 0.5

    def test_conditional_density_zero(self):
        resp = "The answer is 42."
        vec = extractor.extract("q", resp)
        assert vec.conditional_density == 0.0


# ---------------------------------------------------------------------------
# Lexical diversity
# ---------------------------------------------------------------------------

class TestLexicalDiversity:
    def test_lexical_diversity_repetitive(self):
        resp = "cat cat cat cat cat cat cat cat cat cat"
        vec = extractor.extract("q", resp)
        assert vec.lexical_diversity < 0.2

    def test_lexical_diversity_rich(self):
        resp = "The quick brown fox jumps over the lazy dog near the river bank."
        vec = extractor.extract("q", resp)
        assert vec.lexical_diversity > 0.5


# ---------------------------------------------------------------------------
# Uppercase ratio
# ---------------------------------------------------------------------------

class TestUppercaseRatio:
    def test_uppercase_ratio_has_caps(self):
        resp = "This is VERY IMPORTANT and should be noted."
        vec = extractor.extract("q", resp)
        assert vec.uppercase_ratio > 0

    def test_uppercase_ratio_none(self):
        resp = "all words are lowercase here nothing shouted"
        vec = extractor.extract("q", resp)
        assert vec.uppercase_ratio == 0.0


# ---------------------------------------------------------------------------
# Numeric density
# ---------------------------------------------------------------------------

class TestNumericDensity:
    def test_numeric_density_numbers(self):
        resp = "The score was 42 out of 100 with 3.5 average."
        vec = extractor.extract("q", resp)
        assert vec.numeric_density > 0

    def test_numeric_density_no_numbers(self):
        resp = "There are no numbers in this sentence at all."
        vec = extractor.extract("q", resp)
        assert vec.numeric_density == 0.0


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

class TestEdgeCases:
    def test_empty_response(self):
        vec = extractor.extract("question", "")
        assert vec.response_word_count == 0
        assert vec.hedge_density == 0.0
        arr = vec.to_array()
        assert len(arr) == 12

    def test_empty_query(self):
        vec = extractor.extract("", "Some response text.")
        assert vec.query_word_count == 0
        assert vec.length_ratio == vec.response_word_count  # denom = max(0,1) = 1

    def test_single_word_response(self):
        vec = extractor.extract("q", "Paris")
        assert vec.response_word_count == 1
        assert vec.sentence_count == 1

    def test_thread_safety_no_state_mutation(self):
        """Calling extract twice returns independent results."""
        vec1 = extractor.extract("a", "alpha beta gamma")
        vec2 = extractor.extract("b", "delta epsilon zeta theta")
        assert vec1.response_word_count == 3
        assert vec2.response_word_count == 4
