"""Tests for rbsp.core.highlight -- search result highlighting."""

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(__file__))
from conftest import RBSPTestCase, SAMPLE_PROSE

from rbsp.core.highlight import (
    Fragment,
    HighlightConfig,
    Highlighter,
    _lower_set,
)


# -----------------------------------------------------------------------
# HighlightConfig defaults
# -----------------------------------------------------------------------


class TestHighlightConfigDefaults(unittest.TestCase):
    """Verify default configuration values."""

    def test_default_tags(self):
        cfg = HighlightConfig()
        self.assertEqual(cfg.pre_tag, "<b>")
        self.assertEqual(cfg.post_tag, "</b>")

    def test_default_fragment_size(self):
        self.assertEqual(HighlightConfig().fragment_size, 150)

    def test_default_num_fragments(self):
        self.assertEqual(HighlightConfig().num_fragments, 3)

    def test_default_separator(self):
        self.assertEqual(HighlightConfig().separator, " ... ")

    def test_custom_config(self):
        cfg = HighlightConfig(
            pre_tag="<em>",
            post_tag="</em>",
            fragment_size=80,
            num_fragments=5,
            separator=" | ",
        )
        self.assertEqual(cfg.pre_tag, "<em>")
        self.assertEqual(cfg.post_tag, "</em>")
        self.assertEqual(cfg.fragment_size, 80)
        self.assertEqual(cfg.num_fragments, 5)
        self.assertEqual(cfg.separator, " | ")


# -----------------------------------------------------------------------
# Fragment dataclass
# -----------------------------------------------------------------------


class TestFragmentDataclass(unittest.TestCase):
    """Fragment stores text, score, and offsets."""

    def test_fields(self):
        f = Fragment(text="hello world", score=1.5, start=0, end=11)
        self.assertEqual(f.text, "hello world")
        self.assertAlmostEqual(f.score, 1.5)
        self.assertEqual(f.start, 0)
        self.assertEqual(f.end, 11)


# -----------------------------------------------------------------------
# Highlighter — highlight_field (no fragmentation)
# -----------------------------------------------------------------------


class TestHighlightField(unittest.TestCase):
    """highlight_field wraps terms in the full text."""

    def setUp(self):
        self.h = Highlighter()

    def test_single_term(self):
        result = self.h.highlight_field("the quick brown fox", {"quick"})
        self.assertEqual(result, "the <b>quick</b> brown fox")

    def test_multiple_terms(self):
        result = self.h.highlight_field("the quick brown fox", {"quick", "fox"})
        self.assertIn("<b>quick</b>", result)
        self.assertIn("<b>fox</b>", result)

    def test_case_insensitive_matching(self):
        result = self.h.highlight_field("The Quick Brown Fox", {"quick", "fox"})
        self.assertIn("<b>Quick</b>", result)
        self.assertIn("<b>Fox</b>", result)

    def test_no_matches(self):
        result = self.h.highlight_field("the quick brown fox", {"zebra"})
        self.assertEqual(result, "the quick brown fox")

    def test_empty_text(self):
        result = self.h.highlight_field("", {"quick"})
        self.assertEqual(result, "")

    def test_empty_terms(self):
        result = self.h.highlight_field("the quick brown fox", set())
        self.assertEqual(result, "the quick brown fox")

    def test_custom_tags(self):
        cfg = HighlightConfig(pre_tag="[", post_tag="]")
        h = Highlighter(cfg)
        result = h.highlight_field("the quick brown fox", {"quick"})
        self.assertEqual(result, "the [quick] brown fox")

    def test_preserves_original_casing(self):
        result = self.h.highlight_field("Python is Great", {"python"})
        self.assertIn("<b>Python</b>", result)

    def test_all_words_match(self):
        result = self.h.highlight_field("hello world", {"hello", "world"})
        self.assertEqual(result, "<b>hello</b> <b>world</b>")


# -----------------------------------------------------------------------
# Highlighter — get_fragments
# -----------------------------------------------------------------------


class TestGetFragments(unittest.TestCase):
    """get_fragments returns scored Fragment objects."""

    def test_short_text_single_fragment(self):
        h = Highlighter()
        frags = h.get_fragments("hello world", {"hello"})
        self.assertEqual(len(frags), 1)
        self.assertEqual(frags[0].text, "hello world")
        self.assertGreater(frags[0].score, 0.0)
        self.assertEqual(frags[0].start, 0)
        self.assertEqual(frags[0].end, 11)

    def test_long_text_produces_multiple_fragments(self):
        long_text = "The quick brown fox. " * 40  # ~840 chars
        h = Highlighter(HighlightConfig(fragment_size=100))
        frags = h.get_fragments(long_text, {"fox"})
        self.assertGreater(len(frags), 1)

    def test_empty_text_returns_empty(self):
        h = Highlighter()
        self.assertEqual(h.get_fragments("", {"hello"}), [])

    def test_empty_terms_returns_empty(self):
        h = Highlighter()
        self.assertEqual(h.get_fragments("hello world", set()), [])

    def test_no_matching_terms_score_zero(self):
        h = Highlighter()
        frags = h.get_fragments("hello world", {"zebra"})
        self.assertEqual(len(frags), 1)
        self.assertAlmostEqual(frags[0].score, 0.0)

    def test_fragment_offsets_cover_full_text(self):
        text = "word " * 200  # 1000 chars
        h = Highlighter(HighlightConfig(fragment_size=100))
        frags = h.get_fragments(text, {"word"})
        starts = sorted(f.start for f in frags)
        self.assertEqual(starts[0], 0)
        ends = sorted(f.end for f in frags)
        self.assertEqual(ends[-1], len(text))

    def test_fragments_do_not_overlap(self):
        text = "word " * 200
        h = Highlighter(HighlightConfig(fragment_size=100))
        frags = h.get_fragments(text, {"word"})
        frags.sort(key=lambda f: f.start)
        for i in range(len(frags) - 1):
            self.assertLessEqual(frags[i].end, frags[i + 1].start)


# -----------------------------------------------------------------------
# Highlighter — fragment scoring
# -----------------------------------------------------------------------


class TestFragmentScoring(unittest.TestCase):
    """Score reflects position and unique term coverage."""

    def setUp(self):
        self.h = Highlighter()

    def test_earlier_match_scores_higher(self):
        # Fragment where match is first word vs last word
        frags_early = self.h.get_fragments("fox is fast", {"fox"})
        frags_late = self.h.get_fragments("very fast fox", {"fox"})
        self.assertGreater(frags_early[0].score, frags_late[0].score)

    def test_more_unique_terms_boost_score(self):
        one_term = self.h.get_fragments("the fox is quick", {"fox"})
        two_terms = self.h.get_fragments("the fox is quick", {"fox", "quick"})
        self.assertGreater(two_terms[0].score, one_term[0].score)

    def test_repeated_term_adds_to_score(self):
        once = self.h.get_fragments("the fox ran", {"fox"})
        twice = self.h.get_fragments("the fox saw fox", {"fox"})
        self.assertGreater(twice[0].score, once[0].score)

    def test_no_match_zero_score(self):
        frags = self.h.get_fragments("nothing here", {"zebra"})
        self.assertAlmostEqual(frags[0].score, 0.0)


# -----------------------------------------------------------------------
# Highlighter — highlight (full pipeline)
# -----------------------------------------------------------------------


class TestHighlightFullPipeline(unittest.TestCase):
    """highlight() produces the complete highlighted string."""

    def test_short_text_highlighted(self):
        h = Highlighter()
        result = h.highlight("the quick brown fox", {"quick"})
        self.assertEqual(result, "the <b>quick</b> brown fox")

    def test_empty_text_returns_empty(self):
        h = Highlighter()
        self.assertEqual(h.highlight("", {"quick"}), "")

    def test_empty_terms_returns_text(self):
        h = Highlighter()
        self.assertEqual(h.highlight("hello", set()), "hello")

    def test_separator_between_fragments(self):
        # Long text with matches spread out
        sentences = [f"Sentence {i} about dogs." for i in range(30)]
        sentences[0] = "The fox jumped quickly."
        sentences[15] = "The fox ran far."
        sentences[29] = "The fox sleeps now."
        text = " ".join(sentences)
        cfg = HighlightConfig(fragment_size=80, num_fragments=3, separator=" | ")
        h = Highlighter(cfg)
        result = h.highlight(text, {"fox"})
        self.assertIn(" | ", result)

    def test_fragments_in_document_order(self):
        # Build a text where the highest-scoring fragment is not the first
        parts = ["alpha beta gamma. " * 3]
        parts.append("delta epsilon zeta. " * 3)
        parts.append("alpha alpha alpha alpha. " * 3)
        text = "".join(parts)
        cfg = HighlightConfig(fragment_size=60, num_fragments=2)
        h = Highlighter(cfg)
        result = h.highlight(text, {"alpha"})
        # Even if the last chunk scored highest, output should be ordered
        positions = []
        search_start = 0
        while True:
            idx = result.find("<b>alpha</b>", search_start)
            if idx == -1:
                break
            positions.append(idx)
            search_start = idx + 1
        self.assertEqual(positions, sorted(positions))

    def test_num_fragments_limits_output(self):
        text = "fox " * 500
        cfg = HighlightConfig(fragment_size=50, num_fragments=2)
        h = Highlighter(cfg)
        result = h.highlight(text, {"fox"})
        # Should have at most one separator (between 2 fragments)
        self.assertLessEqual(result.count(cfg.separator), 1)

    def test_highlight_with_sample_prose(self):
        h = Highlighter()
        result = h.highlight(SAMPLE_PROSE, {"word", "god"})
        self.assertIn("<b>", result)
        self.assertIn("</b>", result)


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


class TestHighlighterEdgeCases(unittest.TestCase):
    """Boundary conditions and special inputs."""

    def test_none_config_uses_defaults(self):
        h = Highlighter(None)
        result = h.highlight_field("hello world", {"hello"})
        self.assertIn("<b>hello</b>", result)

    def test_single_character_text(self):
        h = Highlighter()
        result = h.highlight_field("x", {"x"})
        self.assertEqual(result, "<b>x</b>")

    def test_single_character_no_match(self):
        h = Highlighter()
        result = h.highlight_field("x", {"y"})
        self.assertEqual(result, "x")

    def test_special_regex_chars_in_text(self):
        h = Highlighter()
        # Should not crash on regex-special chars
        result = h.highlight_field("value (foo) [bar]", {"foo"})
        self.assertIn("<b>foo</b>", result)

    def test_unicode_text(self):
        h = Highlighter()
        result = h.highlight_field("der schnelle Fuchs", {"schnelle"})
        self.assertIn("<b>schnelle</b>", result)

    def test_numeric_words(self):
        h = Highlighter()
        result = h.highlight_field("version 42 released", {"42"})
        self.assertIn("<b>42</b>", result)

    def test_mixed_case_query_terms(self):
        h = Highlighter()
        result = h.highlight_field("The Quick Fox", {"THE", "FOX"})
        self.assertIn("<b>The</b>", result)
        self.assertIn("<b>Fox</b>", result)

    def test_very_large_fragment_size(self):
        h = Highlighter(HighlightConfig(fragment_size=100000))
        result = h.highlight("small text", {"small"})
        self.assertEqual(result, "<b>small</b> text")

    def test_fragment_size_one(self):
        # Extreme but should not crash
        h = Highlighter(HighlightConfig(fragment_size=1))
        result = h.get_fragments("hello world", {"hello"})
        self.assertGreater(len(result), 0)


# -----------------------------------------------------------------------
# _lower_set helper
# -----------------------------------------------------------------------


class TestLowerSet(unittest.TestCase):
    def test_lowercases_all(self):
        self.assertEqual(_lower_set({"FOO", "Bar", "baz"}), {"foo", "bar", "baz"})

    def test_empty(self):
        self.assertEqual(_lower_set(set()), set())


if __name__ == "__main__":
    unittest.main()
