"""Tests for CRX Refined (cluster-then-infer)."""

import pytest
from bex.crx_refined import crx_refined, crx_with_confidence, _cluster_by_structure
from bex.crx import CRX
from bex.grammar import (
    Symbol, Concat, Alt, Plus, Optional, Star, Epsilon, Empty,
    match, alphabet,
)


class TestClusterByStructure:
    def test_empty(self):
        assert _cluster_by_structure([]) == {}

    def test_single_sequence(self):
        result = _cluster_by_structure([['a', 'b', 'c']])
        assert len(result) == 1
        assert ('a', 'c', 'short') in result

    def test_same_start_end_same_length(self):
        seqs = [['a', 'b', 'c'], ['a', 'd', 'c']]
        result = _cluster_by_structure(seqs)
        assert len(result) == 1

    def test_different_start_end(self):
        seqs = [['a', 'b', 'c'], ['x', 'y', 'z']]
        result = _cluster_by_structure(seqs)
        assert len(result) == 2

    def test_length_buckets(self):
        seqs = [
            ['a', 'b', 'a'],
            ['a', 'b', 'c', 'a'],
            ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'a'],
            ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'a'],
        ]
        result = _cluster_by_structure(seqs)
        keys = set(result.keys())
        assert ('a', 'a', 'short') in keys
        assert ('a', 'a', 'med') in keys
        assert ('a', 'a', 'long') in keys


class TestCrxRefined:
    def test_empty(self):
        assert isinstance(crx_refined([]), Epsilon)

    def test_single_sequence(self):
        result = crx_refined([['a', 'b', 'c']])
        assert result is not None
        assert match(result, ['a', 'b', 'c'])

    def test_identical_sequences(self):
        seqs = [['a', 'b', 'c']] * 5
        result = crx_refined(seqs)
        alpha = alphabet(result)
        assert 'a' in alpha
        assert 'b' in alpha

    def test_linear_pattern(self):
        seqs = [['a', 'b', 'c'], ['a', 'b', 'c'], ['a', 'b', 'c']]
        result = crx_refined(seqs)
        assert result == CRX().infer(seqs)

    def test_branching_pattern(self):
        seqs = [['a', 'b', 'c'], ['a', 'd', 'e'], ['a', 'b', 'e']]
        result = crx_refined(seqs)
        assert result is not None
        alpha = alphabet(result)
        assert 'a' in alpha

    def test_falls_back_to_standard(self):
        seqs = [['a'], ['b'], ['c'], ['d']]
        result = crx_refined(seqs, min_cluster=2)
        assert result == CRX().infer(seqs)


class TestCrxWithConfidence:
    def test_empty(self):
        result = crx_with_confidence([])
        assert isinstance(result['grammar'], Epsilon)
        assert result['confidence'] == 1.0
        assert result['n_clusters'] == 0

    def test_returns_all_fields(self):
        seqs = [['a', 'b', 'c'], ['a', 'b', 'c']]
        result = crx_with_confidence(seqs)
        assert 'grammar' in result
        assert 'confidence' in result
        assert 'n_clusters' in result
        assert 'largest_cluster' in result
        assert 'n_sequences' in result

    def test_confidence_range(self):
        seqs = [['a', 'b', 'c'], ['a', 'd', 'e']]
        result = crx_with_confidence(seqs)
        assert 0.0 <= result['confidence'] <= 1.0

    def test_confident_when_tight(self):
        seqs = [['a', 'b', 'c']] * 10
        result = crx_with_confidence(seqs)
        assert result['confidence'] >= 0.9

    def test_less_confident_when_diverse(self):
        seqs = [
            ['a', 'b', 'c', 'z'],
            ['a', 'x', 'y', 'z'],
            ['a', 'p', 'q', 'z'],
            ['a', 'r', 's', 'z'],
        ]
        result = crx_with_confidence(seqs)
        assert result['n_clusters'] >= 1


class TestComparisonWithStandard:
    """Compare refined vs standard CRX on various inputs."""

    def test_linear_same_result(self):
        seqs = [['a', 'b', 'c']] * 5
        assert crx_refined(seqs) == CRX().infer(seqs)

    def test_single_symbol(self):
        seqs = [['a']] * 5
        result = crx_refined(seqs)
        assert isinstance(result, Symbol) and result.value == 'a'

    def test_two_symbols(self):
        seqs = [['a', 'b']] * 5
        result = crx_refined(seqs)
        assert isinstance(result, Concat) and len(result.parts) == 2

    def test_disjunction(self):
        seqs = [['a', 'b'], ['a', 'c']]
        result = crx_refined(seqs)
        assert isinstance(result, Alt) or (isinstance(result, Concat) and any(isinstance(p, Alt) for p in result.parts))
