"""
Pure-logic tests for BrowseCompEntityStrategy and EntityKnowledgeGraph.

Tests entity graph operations, entity type identification, specificity
scoring, constraint term extraction, entity candidate extraction, entity
search generation, and knowledge summarization — no LLM or search calls.
"""

from unittest.mock import Mock

from local_deep_research.advanced_search_system.constraints.base_constraint import (
    Constraint,
    ConstraintType,
)
from local_deep_research.advanced_search_system.strategies.browsecomp_entity_strategy import (
    BrowseCompEntityStrategy,
    EntityCandidate,
    EntityKnowledgeGraph,
)


def _entity(name, entity_type="company", **kwargs):
    """Build an EntityCandidate with sensible defaults."""
    return EntityCandidate(
        name=name,
        entity_type=entity_type,
        aliases=kwargs.get("aliases", []),
        properties=kwargs.get("properties", {}),
        sources=kwargs.get("sources", []),
        confidence=kwargs.get("confidence", 0.0),
        constraint_matches=kwargs.get("constraint_matches", {}),
    )


def _constraint(id, ctype=ConstraintType.PROPERTY, value="v"):
    return Constraint(id=id, type=ctype, description="", value=value)


def _make_strategy():
    """Build a minimal Mock with entity_patterns matching the real init."""
    s = Mock(spec=[])
    s.entity_patterns = {
        "company": [
            "company",
            "corporation",
            "group",
            "firm",
            "business",
            "conglomerate",
        ],
        "person": ["person", "individual", "character", "figure", "people"],
        "event": [
            "event",
            "incident",
            "occurrence",
            "game",
            "match",
            "competition",
        ],
        "location": [
            "place",
            "location",
            "city",
            "country",
            "region",
            "area",
        ],
        "product": ["product", "item", "device", "software", "app", "tool"],
    }
    s.knowledge_graph = EntityKnowledgeGraph()
    return s


# ---------------------------------------------------------------------------
# EntityKnowledgeGraph.add_entity
# ---------------------------------------------------------------------------


class TestEntityKnowledgeGraphAddEntity:
    """Verify merge-on-add and fresh-insert logic."""

    def test_add_new_entity(self):
        """New entity is stored by name."""
        g = EntityKnowledgeGraph()
        e = _entity("Acme Corp")
        g.add_entity(e)
        assert "Acme Corp" in g.entities

    def test_merge_aliases(self):
        """Repeated add merges and deduplicates aliases."""
        g = EntityKnowledgeGraph()
        g.add_entity(_entity("X", aliases=["a", "b"]))
        g.add_entity(_entity("X", aliases=["b", "c"]))
        assert set(g.entities["X"].aliases) == {"a", "b", "c"}

    def test_merge_properties(self):
        """Properties dict is updated on merge."""
        g = EntityKnowledgeGraph()
        g.add_entity(_entity("X", properties={"k1": "v1"}))
        g.add_entity(_entity("X", properties={"k2": "v2"}))
        assert g.entities["X"].properties == {"k1": "v1", "k2": "v2"}

    def test_merge_sources(self):
        """Sources are merged and deduplicated."""
        g = EntityKnowledgeGraph()
        g.add_entity(_entity("X", sources=["s1", "s2"]))
        g.add_entity(_entity("X", sources=["s2", "s3"]))
        assert set(g.entities["X"].sources) == {"s1", "s2", "s3"}

    def test_merge_constraint_matches(self):
        """Constraint matches are updated on merge."""
        g = EntityKnowledgeGraph()
        g.add_entity(_entity("X", constraint_matches={"c1": 0.5}))
        g.add_entity(_entity("X", constraint_matches={"c2": 0.9}))
        assert g.entities["X"].constraint_matches == {"c1": 0.5, "c2": 0.9}


# ---------------------------------------------------------------------------
# EntityKnowledgeGraph.get_entities_by_constraint
# ---------------------------------------------------------------------------


class TestGetEntitiesByConstraint:
    """Verify threshold filtering and confidence-descending sort."""

    def test_filters_below_threshold(self):
        """Entities below min_confidence are excluded."""
        g = EntityKnowledgeGraph()
        g.add_entity(_entity("A", constraint_matches={"c1": 0.3}))
        g.add_entity(_entity("B", constraint_matches={"c1": 0.7}))
        result = g.get_entities_by_constraint("c1", min_confidence=0.5)
        assert [e.name for e in result] == ["B"]

    def test_sorted_descending(self):
        """Results are sorted by confidence descending."""
        g = EntityKnowledgeGraph()
        g.add_entity(_entity("A", constraint_matches={"c1": 0.6}))
        g.add_entity(_entity("B", constraint_matches={"c1": 0.9}))
        g.add_entity(_entity("C", constraint_matches={"c1": 0.75}))
        result = g.get_entities_by_constraint("c1", min_confidence=0.5)
        assert [e.name for e in result] == ["B", "C", "A"]

    def test_no_matching_constraint(self):
        """Entities without the constraint are excluded."""
        g = EntityKnowledgeGraph()
        g.add_entity(_entity("A", constraint_matches={"c2": 0.9}))
        assert g.get_entities_by_constraint("c1") == []

    def test_empty_graph(self):
        """Empty graph returns empty list."""
        g = EntityKnowledgeGraph()
        assert g.get_entities_by_constraint("c1") == []


# ---------------------------------------------------------------------------
# _identify_entity_type
# ---------------------------------------------------------------------------


class TestIdentifyEntityType:
    """Verify keyword-based entity type identification."""

    def _call(self, query):
        s = _make_strategy()
        return BrowseCompEntityStrategy._identify_entity_type(s, query)

    def test_company_keyword(self):
        """'corporation' -> 'company'."""
        assert self._call("Which corporation was founded in 1990?") == "company"

    def test_person_keyword(self):
        """'individual' -> 'person'."""
        assert self._call("Which individual won the award?") == "person"

    def test_event_keyword(self):
        """'competition' -> 'event'."""
        assert self._call("What competition took place in 2020?") == "event"

    def test_location_keyword(self):
        """'city' -> 'location'."""
        assert self._call("What city is known for X?") == "location"

    def test_product_keyword(self):
        """'software' -> 'product'."""
        assert self._call("Which software was released?") == "product"

    def test_who_fallback(self):
        """'who' without other keywords -> 'person'."""
        assert self._call("Who did this?") == "person"

    def test_which_fallback(self):
        """'which' without other keywords -> 'product'."""
        assert self._call("Which one is best?") == "product"

    def test_what_company_fallback(self):
        """'what' + 'company' -> 'company'."""
        assert self._call("What company does this?") == "company"

    def test_generic_fallback(self):
        """No keywords at all -> 'entity'."""
        assert self._call("Tell me about the answer") == "entity"


# ---------------------------------------------------------------------------
# entity_specificity
# ---------------------------------------------------------------------------


class TestEntitySpecificity:
    """Verify multi-factor specificity scoring."""

    def _call(self, entity):
        return BrowseCompEntityStrategy.entity_specificity(Mock(), entity)

    def test_single_short_word(self):
        """Short single word: length * 0.1 + 1 word * 2.0."""
        score = self._call("Abc")
        assert abs(score - (3 * 0.1 + 1 * 2.0)) < 1e-9

    def test_company_suffix_bonus(self):
        """Company suffix adds 10.0."""
        score = self._call("Acme Corp")
        assert score >= 10.0

    def test_hyphen_bonus(self):
        """Hyphenated entity gets +5.0."""
        base = self._call("PRAN")
        with_hyphen = self._call("PRAN-RFL")
        # Hyphen adds 5.0 (and possibly more from length/words)
        assert with_hyphen > base + 4.0

    def test_all_uppercase_bonus(self):
        """All-uppercase entity >= 3 chars gets +8.0."""
        score = self._call("IBM")
        # 3 * 0.1 + 1 * 2.0 + 8.0 = 10.3
        assert abs(score - 10.3) < 1e-9

    def test_multi_word_higher(self):
        """More words -> higher score due to word_count * 2.0."""
        one_word = self._call("Apple")
        two_words = self._call("Apple Inc")
        assert two_words > one_word


# ---------------------------------------------------------------------------
# _extract_constraint_terms
# ---------------------------------------------------------------------------


class TestExtractConstraintTerms:
    """Verify prefix stripping, regex extraction, and word filtering."""

    def _call(self, value, ctype=ConstraintType.PROPERTY):
        c = _constraint("c1", ctype, value)
        # Use real ConstraintType enum values (lowercase)
        # The method checks constraint.type.value against uppercase strings
        return BrowseCompEntityStrategy._extract_constraint_terms(Mock(), c)

    def test_long_words_extracted(self):
        """Words > 4 chars (not stopwords) are extracted."""
        result = self._call("something really interesting")
        assert "something" in result
        assert "really" in result
        assert "interesting" in result

    def test_capped_at_five(self):
        """No more than 5 terms returned."""
        result = self._call("alpha bravo charlie delta echo foxtrot")
        assert len(result) <= 5

    def test_prefix_stripped(self):
        """'Must be' prefix is removed before extraction."""
        result = self._call("Must be a large entity")
        # "Must" and "be" are stripped; remaining words > 4 chars kept
        assert "large" in result
        assert "entity" in result

    def test_short_words_excluded(self):
        """Words <= 4 chars are not included as general terms."""
        result = self._call("a big cat")
        # None of these words > 4 chars
        assert all(len(t) > 4 or t.isdigit() for t in result) or result == []

    def test_stopword_excluded(self):
        """Modal verbs in the stopword list are excluded."""
        result = self._call("should would could must")
        # All 4-5 char words that are in the exclusion list
        assert "should" not in result
        assert "would" not in result
        assert "could" not in result


# ---------------------------------------------------------------------------
# extract_entity_candidates
# ---------------------------------------------------------------------------


class TestExtractEntityCandidates:
    """Verify regex-based candidate extraction and specificity sort."""

    def _call(self, constraint_values):
        s = _make_strategy()
        # Bind the real entity_specificity method so sorted() can use it
        s.entity_specificity = (
            BrowseCompEntityStrategy.entity_specificity.__get__(s)
        )
        constraints = [
            _constraint(f"c{i}", value=v)
            for i, v in enumerate(constraint_values)
        ]
        return BrowseCompEntityStrategy.extract_entity_candidates(
            s, constraints
        )

    def test_extracts_proper_nouns(self):
        """Capitalized multi-word names are extracted."""
        result = self._call(["John Smith is the founder"])
        assert "John Smith" in result or "John" in result

    def test_extracts_company_patterns(self):
        """Company suffixes (Group, Inc, etc.) are captured."""
        result = self._call(["Founded by Acme Corp in 2000"])
        assert any("Acme" in r for r in result)

    def test_extracts_acronyms(self):
        """All-uppercase acronyms >= 2 chars are extracted."""
        result = self._call(["The PRAN-RFL group"])
        # Should find PRAN-RFL or PRAN or RFL
        assert any("PRAN" in r or "RFL" in r for r in result)

    def test_deduplicated(self):
        """Duplicate names across constraints are removed."""
        result = self._call(["John Smith", "John Smith again"])
        john_count = sum(1 for r in result if r == "John Smith")
        assert john_count <= 1

    def test_sorted_by_specificity(self):
        """Results are sorted by specificity score descending."""
        s = _make_strategy()
        s.entity_specificity = (
            BrowseCompEntityStrategy.entity_specificity.__get__(s)
        )
        constraints = [_constraint("c1", value="IBM Corp or Apple")]
        result = BrowseCompEntityStrategy.extract_entity_candidates(
            s, constraints
        )
        if len(result) >= 2:
            scores = [
                BrowseCompEntityStrategy.entity_specificity(s, r)
                for r in result
            ]
            assert scores == sorted(scores, reverse=True)


# ---------------------------------------------------------------------------
# _generate_entity_searches
# ---------------------------------------------------------------------------


class TestGenerateEntitySearches:
    """Verify type-based search query generation with regex extraction."""

    def _call(self, entity_type, constraints):
        return BrowseCompEntityStrategy._generate_entity_searches(
            Mock(), entity_type, constraints
        )

    def test_company_base_searches(self):
        """Company type produces company-related base queries."""
        result = self._call("company", [])
        assert any("companies" in q or "corporation" in q for q in result)

    def test_person_base_searches(self):
        """Person type produces person-related base queries."""
        result = self._call("person", [])
        assert any("people" in q or "person" in q for q in result)

    def test_event_base_searches(self):
        """Event type produces event-related base queries."""
        result = self._call("event", [])
        assert any("events" in q or "event" in q for q in result)

    def test_unknown_type_no_base(self):
        """Unknown entity type produces no base searches."""
        result = self._call("widget", [])
        assert len(result) == 0

    def test_constraint_type_branches_use_uppercase_comparison(self):
        """Constraint type branches compare against uppercase strings but
        ConstraintType enum values are lowercase, so these branches are
        currently unreachable. Verify only base searches are generated."""
        c = _constraint("c1", ConstraintType.STATISTIC, "has 42 employees")
        result = self._call("company", [c])
        # The "STATISTIC" uppercase comparison doesn't match "statistic",
        # so no number-extraction queries are added — only the 3 base queries
        assert len(result) == 3


# ---------------------------------------------------------------------------
# _summarize_knowledge
# ---------------------------------------------------------------------------


class TestSummarizeKnowledge:
    """Verify knowledge summary formatting."""

    def test_empty_graph(self):
        """Empty graph -> empty summary."""
        s = _make_strategy()
        result = BrowseCompEntityStrategy._summarize_knowledge(s)
        assert result == ""

    def test_entities_listed(self):
        """Entities are listed with confidence percentages."""
        s = _make_strategy()
        s.knowledge_graph.add_entity(
            _entity("Acme", confidence=0.85, entity_type="company")
        )
        result = BrowseCompEntityStrategy._summarize_knowledge(s)
        assert "Acme" in result
        assert "85" in result  # 0.85 formatted as 85.00%

    def test_top_five_only(self):
        """Only top 5 entities by confidence are shown."""
        s = _make_strategy()
        for i in range(8):
            s.knowledge_graph.add_entity(
                _entity(f"E{i}", confidence=i * 0.1, entity_type="company")
            )
        result = BrowseCompEntityStrategy._summarize_knowledge(s)
        # Top 5 are E7, E6, E5, E4, E3
        assert "E7" in result
        assert "E0" not in result

    def test_properties_shown(self):
        """Entity properties are displayed."""
        s = _make_strategy()
        s.knowledge_graph.add_entity(
            _entity("X", confidence=0.5, properties={"color": "blue"})
        )
        result = BrowseCompEntityStrategy._summarize_knowledge(s)
        assert "color=blue" in result

    def test_constraint_evidence_shown(self):
        """Constraint evidence section is included when present."""
        s = _make_strategy()
        s.knowledge_graph.add_entity(_entity("X", confidence=0.5))
        s.knowledge_graph.constraint_evidence["some constraint"] = {
            "X": {"score": 0.75}
        }
        result = BrowseCompEntityStrategy._summarize_knowledge(s)
        assert "Constraint verification" in result
        assert "75" in result  # 0.75 formatted as 75.00%
