"""Tests for grammar AST, matching, counting, and GBNF rendering."""

import pytest
from bex.grammar import (
    Symbol, Concat, Alt, Plus, Optional, Star, Epsilon, Empty,
    alphabet, count_words, lang_size, model_cost, match,
)
from bex.gbnf import to_gbnf, to_gbnf_with_rules, grammar_structure_score


# ── AST Construction ──

class TestASTConstruction:
    def test_symbol(self):
        assert Symbol('a') == Symbol('a')
        assert Symbol('a') != Symbol('b')

    def test_concat(self):
        c = Concat([Symbol('a'), Symbol('b')])
        assert c.parts == [Symbol('a'), Symbol('b')]

    def test_alt(self):
        a = Alt([Symbol('a'), Symbol('b')])
        assert a.parts == [Symbol('a'), Symbol('b')]

    def test_plus(self):
        p = Plus(Symbol('a'))
        assert p.child == Symbol('a')

    def test_optional(self):
        o = Optional(Symbol('a'))
        assert o.child == Symbol('a')

    def test_star(self):
        s = Star(Symbol('a'))
        assert s.child == Symbol('a')

    def test_epsilon(self):
        assert Epsilon() == Epsilon()

    def test_empty(self):
        assert Empty() == Empty()


# ── Alphabet ──

class TestAlphabet:
    def test_symbol(self):
        assert alphabet(Symbol('a')) == {'a'}

    def test_concat(self):
        assert alphabet(Concat([Symbol('a'), Symbol('b')])) == {'a', 'b'}

    def test_nested(self):
        g = Concat([Symbol('a'), Plus(Alt([Symbol('b'), Symbol('c')]))])
        assert alphabet(g) == {'a', 'b', 'c'}


# ── Matching ──

class TestMatch:
    def test_symbol(self):
        assert match(Symbol('a'), ['a'])
        assert not match(Symbol('a'), ['b'])
        assert not match(Symbol('a'), [])

    def test_concat(self):
        g = Concat([Symbol('a'), Symbol('b'), Symbol('c')])
        assert match(g, ['a', 'b', 'c'])
        assert not match(g, ['a', 'b'])
        assert not match(g, ['a', 'b', 'c', 'd'])

    def test_alt(self):
        g = Alt([Symbol('a'), Symbol('b')])
        assert match(g, ['a'])
        assert match(g, ['b'])
        assert not match(g, ['c'])

    def test_plus(self):
        g = Plus(Symbol('a'))
        assert match(g, ['a'])
        assert match(g, ['a', 'a'])
        assert match(g, ['a', 'a', 'a'])
        assert not match(g, [])
        assert not match(g, ['b'])

    def test_optional(self):
        g = Optional(Symbol('a'))
        assert match(g, ['a'])
        assert match(g, [])
        assert not match(g, ['b'])

    def test_star(self):
        g = Star(Symbol('a'))
        assert match(g, [])
        assert match(g, ['a'])
        assert match(g, ['a', 'a'])
        assert not match(g, ['b'])

    def test_complex_grammar(self):
        g = Concat([Plus(Symbol('init')), Plus(Symbol('capability')),
                     Concat([Plus(Symbol('invoke')), Symbol('request')])])
        assert match(g, ['init', 'capability', 'invoke', 'request'])
        assert match(g, ['init', 'init', 'capability', 'capability', 'invoke', 'request'])
        assert not match(g, ['init'])


# ── Counting ──

class TestCountWords:
    def test_symbol(self):
        assert count_words(Symbol('a'), 1) == 1
        assert count_words(Symbol('a'), 0) == 0

    def test_epsilon(self):
        assert count_words(Epsilon(), 0) == 1
        assert count_words(Epsilon(), 1) == 0

    def test_empty(self):
        assert count_words(Empty(), 0) == 0

    def test_concat(self):
        g = Concat([Symbol('a'), Symbol('b'), Symbol('c')])
        assert count_words(g, 3) == 1
        assert count_words(g, 2) == 0

    def test_alt(self):
        g = Alt([Symbol('a'), Symbol('b'), Symbol('c')])
        assert count_words(g, 1) == 3

    def test_plus(self):
        g = Plus(Symbol('a'))
        assert count_words(g, 1) == 1
        assert count_words(g, 2) == 1
        assert count_words(g, 0) == 0

    def test_optional(self):
        g = Optional(Symbol('a'))
        assert count_words(g, 0) == 1
        assert count_words(g, 1) == 1

    def test_star(self):
        g = Star(Symbol('a'))
        assert count_words(g, 0) == 1
        assert count_words(g, 1) == 1

    def test_disjunction_plus(self):
        g = Plus(Alt([Symbol('a'), Symbol('b'), Symbol('c')]))
        assert count_words(g, 1) == 3
        assert count_words(g, 2) == 9

    def test_info_plus(self):
        g = Plus(Symbol('info'))
        for l in range(1, 8):
            assert count_words(g, l) == 1


# ── Model cost ──

class TestModelCost:
    def test_symbol(self):
        assert model_cost(Symbol('a')) == 1

    def test_concat(self):
        assert model_cost(Concat([Symbol('a'), Symbol('b'), Symbol('c')])) == 3

    def test_plus(self):
        assert model_cost(Plus(Symbol('a'))) == 1

    def test_nested(self):
        g = Concat([Symbol('a'), Plus(Alt([Symbol('b'), Symbol('c')]))])
        assert model_cost(g) == 3


# ── GBNF Rendering ──

def test_to_gbnf():
    g = Plus(Symbol('init'))
    result = to_gbnf(g)
    assert result == 'root ::= "init"+'


def test_to_gbnf_with_rules():
    g = Alt([Symbol('a'), Symbol('b')])
    result = to_gbnf_with_rules(g, name='choice')
    assert result == 'choice ::= "a" | "b"'


# ── Structure score ──

class TestStructureScore:
    def test_empty(self):
        assert grammar_structure_score(Empty()) == 0.0

    def test_symbol(self):
        assert grammar_structure_score(Symbol('a')) == 0.0

    def test_concat(self):
        g = Concat([Symbol('a'), Symbol('b'), Symbol('c')])
        assert grammar_structure_score(g) > 0.0

    def test_plus(self):
        g = Plus(Symbol('a'))
        assert grammar_structure_score(g) > 0.0
