"""Tests for kOREInference (Algorithm 4: iDRegEx from arXiv 1004.2372)."""

from bex.kore import kOREInference, validate_k_ore
from bex.idregex import is_deterministic
from bex.mdl import mdl_score, model_cost, data_cost


# ── Core inference tests ──

def test_linear_sequence():
    seqs = [
        ['file', 'template', 'command', 'set_fact', 'shell', 'wait_for'],
        ['file', 'template', 'command', 'set_fact', 'shell', 'wait_for'],
    ]
    kore = kOREInference(k_max=3, N=3)
    result = kore.infer(seqs)
    assert result is not None, "Should infer a k-ORE"
    auto, expr, best_k = result
    assert expr is not None
    assert all(t in expr for t in ['file', 'template', 'command', 'set_fact', 'shell', 'wait_for'])
    assert is_deterministic(expr), f"Expression must be deterministic: {expr}"


def test_branching_paths():
    seqs = [
        ['file', 'template', 'setup', 'set_fact', 'shell'],
        ['file', 'template', 'deploy', 'set_fact', 'shell'],
    ]
    kore = kOREInference(k_max=3, N=3)
    result = kore.infer(seqs)
    assert result is not None
    auto, expr, best_k = result
    assert is_deterministic(expr), f"Expression must be deterministic: {expr}"
    assert 'file' in expr and 'template' in expr and 'shell' in expr


def test_optional_element():
    seqs = [
        ['file', 'template', 'shell'],
        ['file', 'template', 'exec', 'shell'],
        ['file', 'template', 'exec', 'exec', 'shell'],
    ]
    kore = kOREInference(k_max=4, N=15)
    result = kore.infer(seqs)
    if result is None:
        return  # stochastic failure
    auto, expr, best_k = result
    assert is_deterministic(expr), f"Expression must be deterministic: {expr}"


def test_looping_element():
    seqs = [
        ['package', 'file', 'template', 'systemd'],
        ['package', 'file', 'template', 'template', 'systemd', 'systemd'],
        ['package', 'file', 'template', 'template', 'template', 'systemd'],
    ]
    kore = kOREInference(k_max=3, N=5)
    result = kore.infer(seqs)
    assert result is not None
    auto, expr, best_k = result
    assert is_deterministic(expr), f"Expression must be deterministic: {expr}"


def test_multiple_alternatives():
    seqs = [
        ['install', 'configure', 'start'],
        ['install', 'configure', 'enable'],
        ['install', 'configure', 'restart'],
    ]
    kore = kOREInference(k_max=3, N=5)
    result = kore.infer(seqs)
    assert result is not None
    auto, expr, best_k = result
    assert is_deterministic(expr), f"Expression must be deterministic: {expr}"


def test_rejects_non_deterministic():
    seqs = [['a'], ['a']]
    kore = kOREInference(k_max=2, N=2)
    result = kore.infer(seqs)
    assert result is not None
    auto, expr, best_k = result
    assert is_deterministic(expr), f"Non-deterministic: {expr}"


def test_empty_input():
    kore = kOREInference(k_max=2, N=2)
    result = kore.infer([])
    assert result is None
    result = kore.infer([[], []])
    assert result is None


def test_single_element_sequences():
    seqs = [['a'], ['b'], ['a'], ['b']]
    kore = kOREInference(k_max=2, N=3)
    result = kore.infer(seqs)
    assert result is not None
    auto, expr, best_k = result
    assert is_deterministic(expr)


def test_infer_returns_best_k():
    seqs = [
        ['a', 'b', 'c'],
        ['a', 'b', 'c', 'd'],
        ['a', 'b', 'd'],
    ]
    kore = kOREInference(k_max=4, N=3)
    result = kore.infer(seqs)
    assert result is not None
    auto, expr, best_k = result
    assert 1 <= best_k <= 4
    assert is_deterministic(expr)


def test_tool_sequences():
    seqs = [
        ['read', 'grep', 'read'],
        ['read', 'glob', 'grep', 'read'],
        ['read', 'bash', 'read'],
        ['glob', 'grep', 'read', 'edit', 'bash'],
        ['read', 'edit', 'bash', 'bash'],
        ['bash', 'read', 'bash'],
    ]
    kore = kOREInference(k_max=3, N=5)
    result = kore.infer(seqs)
    if result is not None:
        auto, expr, best_k = result
        assert is_deterministic(expr)


# ── Edge case tests ──

def test_single_sequence():
    kore = kOREInference(k_max=2, N=3)
    result = kore.infer([['a', 'b', 'c']])
    assert result is not None
    auto, expr, best_k = result
    assert is_deterministic(expr)


def test_many_identical_sequences():
    seqs = [['a', 'b', 'c']] * 20
    kore = kOREInference(k_max=2, N=3)
    result = kore.infer(seqs)
    assert result is not None
    auto, expr, best_k = result
    assert is_deterministic(expr)
    assert 'a' in expr and 'b' in expr and 'c' in expr


def test_xml_like_structured():
    seqs = [
        ['header', 'body', 'footer'],
        ['header', 'body', 'body', 'footer'],
        ['header', 'body', 'body', 'body', 'footer'],
        ['header', 'footer'],
    ]
    kore = kOREInference(k_max=3, N=10)
    result = kore.infer(seqs)
    if result is not None:
        auto, expr, best_k = result
        assert is_deterministic(expr)
        assert 'header' in expr and 'footer' in expr


def test_disjoint_symbols():
    seqs = [
        ['alpha', 'beta'],
        ['gamma', 'delta'],
    ]
    kore = kOREInference(k_max=2, N=3)
    result = kore.infer(seqs)
    if result is not None:
        auto, expr, best_k = result
        assert is_deterministic(expr)


def test_k1_vs_k2_selection():
    seqs = [
        ['a', 'a', 'b'],
        ['a', 'b'],
        ['a', 'a', 'a', 'b'],
    ]
    kore = kOREInference(k_max=3, N=5)
    result = kore.infer(seqs)
    assert result is not None
    auto, expr, best_k = result
    assert is_deterministic(expr)


def test_all_same_symbol():
    seqs = [
        ['a', 'a'],
        ['a', 'a', 'a'],
        ['a'],
    ]
    kore = kOREInference(k_max=2, N=5)
    result = kore.infer(seqs)
    if result is not None:
        auto, expr, best_k = result
        assert is_deterministic(expr)


def test_long_sequence():
    seqs = [
        ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
        ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
    ]
    kore = kOREInference(k_max=2, N=5)
    result = kore.infer(seqs)
    if result is not None:
        auto, expr, best_k = result
        assert is_deterministic(expr)


def test_infer_returns_koa():
    kore = kOREInference(k_max=2, N=3)
    result = kore.infer([['a', 'b'], ['a', 'b', 'c']])
    assert result is not None
    auto, expr, best_k = result
    assert hasattr(auto, '_succ'), "Should return a KOA automaton"
    assert hasattr(auto, 'src')
    assert hasattr(auto, 'sink')


def test_different_kmax():
    seqs = [['a', 'b', 'c', 'd', 'e'], ['a', 'b', 'c']]
    kore1 = kOREInference(k_max=1, N=5)
    kore2 = kOREInference(k_max=3, N=5)
    r1 = kore1.infer(seqs)
    r2 = kore2.infer(seqs)
    assert r1 is not None or r2 is not None


# ── validate_k_ore tests ──

def test_validate_k_ore_basic():
    valid, reason = validate_k_ore('a.b.c', 2)
    assert valid, f"a.b.c should be valid for k=2: {reason}"


def test_validate_k_ore_exceeds_k():
    valid, reason = validate_k_ore('a.a.a', 1)
    assert not valid, "a.a.a should fail for k=1"


def test_validate_k_ore_epsilon():
    valid, reason = validate_k_ore('ε', 1)
    assert valid


def test_validate_k_ore_empty():
    valid, reason = validate_k_ore('', 1)
    assert valid


def test_validate_k_ore_disjunction():
    valid, reason = validate_k_ore('(a|b|c)', 2)
    assert valid, f"(a|b|c) should be valid for k=2: {reason}"


def test_validate_k_ore_loop():
    valid, reason = validate_k_ore('a+', 1)
    assert valid, "a+ should be valid for k=1"


def test_validate_k_ore_k0():
    valid, reason = validate_k_ore('a', 0)
    assert not valid, "a should fail for k=0"


# ── MDL scoring tests ──

def test_mdl_model_cost():
    assert model_cost('a.b.c') == 3
    assert model_cost('(a|b)+.c') >= 2
    assert model_cost('ε') >= 0


def test_mdl_data_cost():
    # General expression (a|b)+ has multiple words of length 1+: non-zero cost
    dc = data_cost('(a|b)+', [['a', 'b'], ['b', 'a'], ['a']])
    assert dc > 0, f"data_cost should be > 0 for general expression, got {dc}"
    # Exact expression has cost 0 (log2(1) = 0)
    dc_exact = data_cost('a.b.c', [['a', 'b', 'c']])
    assert dc_exact == 0.0, f"data_cost for exact match should be 0, got {dc_exact}"


def test_mdl_score_lower_is_better():
    score_specific = mdl_score('a.b.c', [['a', 'b', 'c']])
    score_general = mdl_score('(a|b|c)+?', [['a', 'b', 'c']])
    assert score_specific > 0 and score_general > 0


def test_mdl_empty_sequences():
    score = mdl_score('a.b.c', [])
    assert score == model_cost('a.b.c')


# ── Language Size scoring tests (Bex et al. arXiv:1004.2372 §4.3.1) ──

def test_lang_size_score_basic():
    from bex.mdl import lang_size_score
    # Specific grammar: accepts 1 word of each length ≥ 3
    specific = lang_size_score('a.b.c', [['a', 'b', 'c']])
    # Generic grammar: accepts many words at each length
    generic = lang_size_score('(a+b+c)+', [['a', 'b', 'c']])
    assert specific < generic, f"Specific ({specific}) should score lower than generic ({generic})"


def test_lang_size_prefers_specific_over_info_plus():
    """Generic grammar accepts many words at each length; specific accepts few."""
    from bex.mdl import lang_size_score
    # Diverse sequences — specific grammar accepts 1 word at each length,
    # generic (a+b+c)+ accepts many.
    seqs = [['a', 'b', 'c'], ['a', 'b'], ['a', 'c'], ['b', 'c']]
    specific = lang_size_score('a.b.c', seqs)
    generic = lang_size_score('(a+b+c)+', seqs)
    assert specific < generic, f"Specific ({specific}) should beat generic ({generic})"


def test_score_grammar_method_switch():
    from bex.mdl import score_grammar
    seqs = [['a', 'b', 'c']]
    ls = score_grammar('a.b.c', seqs, method='langsize')
    mdl = score_grammar('a.b.c', seqs, method='mdl')
    assert isinstance(ls, (int, float))
    assert isinstance(mdl, (int, float))


def test_score_grammar_invalid_method():
    from bex.mdl import score_grammar
    try:
        score_grammar('a.b.c', [['a']], method='bogus')
        assert False, "Should have raised ValueError"
    except ValueError:
        pass


def test_ensemble_method_param():
    """Ensemble accepts method= parameter and passes it through."""
    from bex.ensemble import infer_ensemble
    seqs = [['a', 'b'], ['a', 'b', 'c']]
    r_ls = infer_ensemble(seqs, method='langsize')
    r_mdl = infer_ensemble(seqs, method='mdl')
    assert r_ls['best'] is not None
    assert r_mdl['best'] is not None


# ── Algorithm 4 paper-faithful tests ──

def test_infer_returns_deterministic():
    for _ in range(5):
        seqs = [['x', 'y'], ['x', 'z']]
        kore = kOREInference(k_max=2, N=2)
        result = kore.infer(seqs)
        if result:
            _, expr, _ = result
            assert is_deterministic(expr), f"Non-deterministic: {expr}"


def test_infer_obeys_k_occurrence():
    seqs = [['a', 'b'], ['a', 'b', 'c']]
    for k in range(1, 4):
        kore = kOREInference(k_max=k, N=5)
        result = kore.infer(seqs)
        if result:
            _, expr, best_k = result
            valid, _ = validate_k_ore(expr, best_k)
            assert valid, f"k={best_k} expression {expr} violates k-occurrence"


def run_all():
    tests = [
        test_linear_sequence,
        test_branching_paths,
        test_optional_element,
        test_looping_element,
        test_multiple_alternatives,
        test_rejects_non_deterministic,
        test_empty_input,
        test_single_element_sequences,
        test_infer_returns_best_k,
        test_tool_sequences,
        test_single_sequence,
        test_many_identical_sequences,
        test_xml_like_structured,
        test_disjoint_symbols,
        test_k1_vs_k2_selection,
        test_all_same_symbol,
        test_long_sequence,
        test_infer_returns_koa,
        test_different_kmax,
        test_validate_k_ore_basic,
        test_validate_k_ore_exceeds_k,
        test_validate_k_ore_epsilon,
        test_validate_k_ore_empty,
        test_validate_k_ore_disjunction,
        test_validate_k_ore_loop,
        test_validate_k_ore_k0,
        test_mdl_model_cost,
        test_mdl_data_cost,
        test_mdl_score_lower_is_better,
        test_mdl_empty_sequences,
        test_lang_size_score_basic,
        test_lang_size_prefers_specific_over_info_plus,
        test_score_grammar_method_switch,
        test_score_grammar_invalid_method,
        test_ensemble_method_param,
        test_infer_returns_deterministic,
        test_infer_obeys_k_occurrence,
    ]
    passed = 0
    failed = 0
    for t in tests:
        try:
            t()
            passed += 1
        except Exception as e:
            import traceback
            print(f"  FAIL {t.__name__}: {e}")
            traceback.print_exc()
            failed += 1
    print(f"\n{passed} passed, {failed} failed")


if __name__ == '__main__':
    run_all()
