"""Tests for rbsp.core.analyzer -- Configurable text analysis pipeline."""

from __future__ import annotations

import os
import sys
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.core.analyzer import (
    Analyzer,
    ASCIIFoldingFilter,
    CodeAnalyzer,
    LengthFilter,
    LowercaseFilter,
    SimpleAnalyzer,
    StandardAnalyzer,
    StandardTokenizer,
    StemFilter,
    StopWordFilter,
    Token,
    WhitespaceTokenizer,
)


# ---------------------------------------------------------------------------
# Token dataclass
# ---------------------------------------------------------------------------

class TestToken(unittest.TestCase):
    """Token is a simple dataclass with text, start, end, position."""

    def test_creation(self):
        t = Token(text="hello", start=0, end=5, position=0)
        self.assertEqual(t.text, "hello")
        self.assertEqual(t.start, 0)
        self.assertEqual(t.end, 5)
        self.assertEqual(t.position, 0)

    def test_equality(self):
        a = Token(text="foo", start=0, end=3, position=0)
        b = Token(text="foo", start=0, end=3, position=0)
        self.assertEqual(a, b)

    def test_mutation(self):
        t = Token(text="Hello", start=0, end=5, position=0)
        t.text = "hello"
        self.assertEqual(t.text, "hello")


# ---------------------------------------------------------------------------
# WhitespaceTokenizer
# ---------------------------------------------------------------------------

class TestWhitespaceTokenizer(unittest.TestCase):
    """WhitespaceTokenizer splits on whitespace, preserving offsets."""

    def setUp(self):
        self.tok = WhitespaceTokenizer()

    def test_simple_sentence(self):
        tokens = self.tok.tokenize("hello world")
        self.assertEqual(len(tokens), 2)
        self.assertEqual(tokens[0].text, "hello")
        self.assertEqual(tokens[1].text, "world")

    def test_offsets(self):
        tokens = self.tok.tokenize("hello world")
        self.assertEqual(tokens[0].start, 0)
        self.assertEqual(tokens[0].end, 5)
        self.assertEqual(tokens[1].start, 6)
        self.assertEqual(tokens[1].end, 11)

    def test_positions_are_sequential(self):
        tokens = self.tok.tokenize("a b c d")
        for i, t in enumerate(tokens):
            self.assertEqual(t.position, i)

    def test_empty_string(self):
        tokens = self.tok.tokenize("")
        self.assertEqual(tokens, [])

    def test_only_whitespace(self):
        tokens = self.tok.tokenize("   \t\n  ")
        self.assertEqual(tokens, [])

    def test_multiple_spaces(self):
        tokens = self.tok.tokenize("hello    world")
        self.assertEqual(len(tokens), 2)
        self.assertEqual(tokens[0].text, "hello")
        self.assertEqual(tokens[1].text, "world")

    def test_preserves_case_and_punctuation(self):
        tokens = self.tok.tokenize("Hello, World!")
        self.assertEqual(tokens[0].text, "Hello,")
        self.assertEqual(tokens[1].text, "World!")

    def test_tabs_and_newlines(self):
        tokens = self.tok.tokenize("a\tb\nc")
        self.assertEqual(len(tokens), 3)
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["a", "b", "c"])


# ---------------------------------------------------------------------------
# StandardTokenizer
# ---------------------------------------------------------------------------

class TestStandardTokenizer(unittest.TestCase):
    """StandardTokenizer splits on non-alnum and camelCase boundaries."""

    def setUp(self):
        self.tok = StandardTokenizer()

    def test_camel_case_split(self):
        tokens = self.tok.tokenize("parseJSON")
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["parse", "JSON"])

    def test_underscore_split(self):
        tokens = self.tok.tokenize("foo_bar_baz")
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["foo", "bar", "baz"])

    def test_hyphen_split(self):
        tokens = self.tok.tokenize("foo-bar-baz")
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["foo", "bar", "baz"])

    def test_mixed_separators(self):
        tokens = self.tok.tokenize("foo_bar-baz")
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["foo", "bar", "baz"])

    def test_camel_case_complex(self):
        """ABCDef -> ABC + Def"""
        tokens = self.tok.tokenize("ABCDef")
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["ABC", "Def"])

    def test_camel_case_simple_lower_upper(self):
        tokens = self.tok.tokenize("camelCase")
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["camel", "Case"])

    def test_multiple_camel_transitions(self):
        tokens = self.tok.tokenize("getHTTPResponseCode")
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["get", "HTTP", "Response", "Code"])

    def test_empty_string(self):
        tokens = self.tok.tokenize("")
        self.assertEqual(tokens, [])

    def test_pure_symbols(self):
        tokens = self.tok.tokenize("!@#$%^&*()")
        self.assertEqual(tokens, [])

    def test_numbers_preserved(self):
        tokens = self.tok.tokenize("version123")
        texts = [t.text for t in tokens]
        self.assertEqual(texts, ["version123"])

    def test_positions_sequential(self):
        tokens = self.tok.tokenize("fooBar baz_qux")
        for i, t in enumerate(tokens):
            self.assertEqual(t.position, i)

    def test_offsets_correct(self):
        tokens = self.tok.tokenize("fooBar")
        self.assertEqual(tokens[0].text, "foo")
        self.assertEqual(tokens[0].start, 0)
        self.assertEqual(tokens[0].end, 3)
        self.assertEqual(tokens[1].text, "Bar")
        self.assertEqual(tokens[1].start, 3)
        self.assertEqual(tokens[1].end, 6)

    def test_single_word(self):
        tokens = self.tok.tokenize("hello")
        self.assertEqual(len(tokens), 1)
        self.assertEqual(tokens[0].text, "hello")


# ---------------------------------------------------------------------------
# LowercaseFilter
# ---------------------------------------------------------------------------

class TestLowercaseFilter(unittest.TestCase):
    """LowercaseFilter lowercases all token text."""

    def setUp(self):
        self.filt = LowercaseFilter()

    def test_lowers_mixed_case(self):
        tokens = [Token("Hello", 0, 5, 0), Token("WORLD", 6, 11, 1)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "hello")
        self.assertEqual(result[1].text, "world")

    def test_already_lowercase(self):
        tokens = [Token("hello", 0, 5, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "hello")

    def test_empty_list(self):
        result = self.filt.filter([])
        self.assertEqual(result, [])

    def test_preserves_offsets(self):
        tokens = [Token("FOO", 10, 13, 3)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].start, 10)
        self.assertEqual(result[0].end, 13)
        self.assertEqual(result[0].position, 3)


# ---------------------------------------------------------------------------
# StopWordFilter
# ---------------------------------------------------------------------------

class TestStopWordFilter(unittest.TestCase):
    """StopWordFilter removes stop words."""

    def test_removes_default_stop_words(self):
        filt = StopWordFilter()
        tokens = [
            Token("the", 0, 3, 0),
            Token("quick", 4, 9, 1),
            Token("and", 10, 13, 2),
            Token("fast", 14, 18, 3),
        ]
        result = filt.filter(tokens)
        texts = [t.text for t in result]
        self.assertEqual(texts, ["quick", "fast"])

    def test_custom_stop_words(self):
        filt = StopWordFilter(stop_words=frozenset({"foo", "bar"}))
        tokens = [
            Token("foo", 0, 3, 0),
            Token("hello", 4, 9, 1),
            Token("bar", 10, 13, 2),
        ]
        result = filt.filter(tokens)
        texts = [t.text for t in result]
        self.assertEqual(texts, ["hello"])

    def test_no_stop_words_present(self):
        filt = StopWordFilter()
        tokens = [Token("quick", 0, 5, 0), Token("fast", 6, 10, 1)]
        result = filt.filter(tokens)
        self.assertEqual(len(result), 2)

    def test_all_stop_words(self):
        filt = StopWordFilter()
        tokens = [Token("the", 0, 3, 0), Token("and", 4, 7, 1)]
        result = filt.filter(tokens)
        self.assertEqual(result, [])


# ---------------------------------------------------------------------------
# StemFilter
# ---------------------------------------------------------------------------

class TestStemFilter(unittest.TestCase):
    """StemFilter applies Porter stemming."""

    def setUp(self):
        self.filt = StemFilter()

    def test_stems_common_words(self):
        tokens = [Token("running", 0, 7, 0), Token("cats", 8, 12, 1)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "run")
        self.assertEqual(result[1].text, "cat")

    def test_already_stemmed(self):
        tokens = [Token("run", 0, 3, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "run")

    def test_empty_list(self):
        result = self.filt.filter([])
        self.assertEqual(result, [])


# ---------------------------------------------------------------------------
# LengthFilter
# ---------------------------------------------------------------------------

class TestLengthFilter(unittest.TestCase):
    """LengthFilter removes tokens shorter than min_length."""

    def test_default_min_length_is_2(self):
        filt = LengthFilter()
        tokens = [Token("a", 0, 1, 0), Token("bc", 2, 4, 1), Token("def", 5, 8, 2)]
        result = filt.filter(tokens)
        texts = [t.text for t in result]
        self.assertEqual(texts, ["bc", "def"])

    def test_custom_min_length(self):
        filt = LengthFilter(min_length=4)
        tokens = [Token("hi", 0, 2, 0), Token("hey", 3, 6, 1), Token("hello", 7, 12, 2)]
        result = filt.filter(tokens)
        texts = [t.text for t in result]
        self.assertEqual(texts, ["hello"])

    def test_empty_list(self):
        filt = LengthFilter()
        result = filt.filter([])
        self.assertEqual(result, [])

    def test_all_too_short(self):
        filt = LengthFilter(min_length=5)
        tokens = [Token("a", 0, 1, 0), Token("bb", 2, 4, 1)]
        result = filt.filter(tokens)
        self.assertEqual(result, [])


# ---------------------------------------------------------------------------
# ASCIIFoldingFilter
# ---------------------------------------------------------------------------

class TestASCIIFoldingFilter(unittest.TestCase):
    """ASCIIFoldingFilter normalizes Unicode to ASCII."""

    def setUp(self):
        self.filt = ASCIIFoldingFilter()

    def test_accented_characters(self):
        tokens = [Token("caf\u00e9", 0, 4, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "cafe")

    def test_umlaut(self):
        tokens = [Token("\u00fcber", 0, 4, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "uber")

    def test_german_eszett(self):
        tokens = [Token("stra\u00dfe", 0, 6, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "strasse")

    def test_tilde_n(self):
        tokens = [Token("espa\u00f1ol", 0, 7, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "espanol")

    def test_cedilla(self):
        tokens = [Token("fran\u00e7ais", 0, 8, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "francais")

    def test_already_ascii(self):
        tokens = [Token("hello", 0, 5, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "hello")

    def test_uppercase_accented(self):
        tokens = [Token("\u00c9cole", 0, 5, 0)]
        result = self.filt.filter(tokens)
        self.assertEqual(result[0].text, "Ecole")

    def test_empty_list(self):
        result = self.filt.filter([])
        self.assertEqual(result, [])


# ---------------------------------------------------------------------------
# Analyzer
# ---------------------------------------------------------------------------

class TestAnalyzer(unittest.TestCase):
    """Analyzer chains a tokenizer with filters."""

    def test_analyze_returns_tokens(self):
        analyzer = Analyzer(
            tokenizer=WhitespaceTokenizer(),
            filters=[LowercaseFilter()],
        )
        tokens = analyzer.analyze("Hello World")
        self.assertEqual(len(tokens), 2)
        self.assertEqual(tokens[0].text, "hello")
        self.assertEqual(tokens[1].text, "world")

    def test_analyze_to_strings(self):
        analyzer = Analyzer(
            tokenizer=WhitespaceTokenizer(),
            filters=[LowercaseFilter()],
        )
        result = analyzer.analyze_to_strings("Hello World")
        self.assertEqual(result, ["hello", "world"])

    def test_no_filters(self):
        analyzer = Analyzer(
            tokenizer=WhitespaceTokenizer(),
            filters=[],
        )
        result = analyzer.analyze_to_strings("Hello World")
        self.assertEqual(result, ["Hello", "World"])

    def test_multiple_filters(self):
        analyzer = Analyzer(
            tokenizer=WhitespaceTokenizer(),
            filters=[LowercaseFilter(), LengthFilter(min_length=4)],
        )
        result = analyzer.analyze_to_strings("I am a developer")
        self.assertEqual(result, ["developer"])

    def test_empty_input(self):
        analyzer = Analyzer(
            tokenizer=WhitespaceTokenizer(),
            filters=[LowercaseFilter()],
        )
        result = analyzer.analyze("")
        self.assertEqual(result, [])


# ---------------------------------------------------------------------------
# Factory functions
# ---------------------------------------------------------------------------

class TestStandardAnalyzer(unittest.TestCase):
    """StandardAnalyzer: StandardTokenizer + Lowercase + StopWord + Stem + Length."""

    def test_basic_sentence(self):
        analyzer = StandardAnalyzer()
        result = analyzer.analyze_to_strings("The quick brown fox")
        # "the" removed (stop word); rest lowered and stemmed
        self.assertNotIn("the", result)
        self.assertIn("quick", result)
        self.assertIn("brown", result)
        self.assertIn("fox", result)

    def test_camel_case_splitting(self):
        analyzer = StandardAnalyzer()
        result = analyzer.analyze_to_strings("parseJSON")
        self.assertIn("pars", result)  # "parse" stemmed to "pars"
        self.assertIn("json", result)

    def test_stop_words_removed(self):
        analyzer = StandardAnalyzer()
        result = analyzer.analyze_to_strings("this is a test")
        # "this", "is", "a" are stop words; "test" stays
        self.assertEqual(result, ["test"])

    def test_short_tokens_removed(self):
        analyzer = StandardAnalyzer()
        result = analyzer.analyze_to_strings("I go")
        # "i" is stop word; "go" stems to "go" which is length 2 -- should survive
        self.assertIn("go", result)

    def test_empty_string(self):
        analyzer = StandardAnalyzer()
        result = analyzer.analyze_to_strings("")
        self.assertEqual(result, [])


class TestSimpleAnalyzer(unittest.TestCase):
    """SimpleAnalyzer: WhitespaceTokenizer + Lowercase."""

    def test_lowercases(self):
        analyzer = SimpleAnalyzer()
        result = analyzer.analyze_to_strings("Hello World")
        self.assertEqual(result, ["hello", "world"])

    def test_preserves_punctuation(self):
        analyzer = SimpleAnalyzer()
        result = analyzer.analyze_to_strings("Hello, World!")
        self.assertEqual(result, ["hello,", "world!"])

    def test_empty_string(self):
        analyzer = SimpleAnalyzer()
        result = analyzer.analyze_to_strings("")
        self.assertEqual(result, [])


class TestCodeAnalyzer(unittest.TestCase):
    """CodeAnalyzer: StandardTokenizer + Lowercase + Length(2). No stemming."""

    def test_camel_case_splitting(self):
        analyzer = CodeAnalyzer()
        result = analyzer.analyze_to_strings("parseJSON")
        self.assertEqual(result, ["parse", "json"])

    def test_no_stemming(self):
        analyzer = CodeAnalyzer()
        result = analyzer.analyze_to_strings("running tests")
        # Without stemming, "running" stays as "running"
        self.assertIn("running", result)
        self.assertIn("tests", result)

    def test_short_tokens_removed(self):
        analyzer = CodeAnalyzer()
        result = analyzer.analyze_to_strings("a big class")
        # "a" is length 1, removed
        self.assertNotIn("a", result)
        self.assertIn("big", result)
        self.assertIn("class", result)

    def test_underscore_identifiers(self):
        analyzer = CodeAnalyzer()
        result = analyzer.analyze_to_strings("my_var_name")
        self.assertEqual(result, ["my", "var", "name"])

    def test_empty_string(self):
        analyzer = CodeAnalyzer()
        result = analyzer.analyze_to_strings("")
        self.assertEqual(result, [])


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