"""Property-based tests for fixpoint computation in the Ink language."""

from hypothesis import given, strategies as st
from ink.ast_nodes import Proposition, SVOClause, ClassificationEdge, Rule
from ink.interpreter import fixpoint, Context


# Generators for test data
@st.composite
def identifiers(draw):
    """Generate valid identifiers (non-variable)."""
    return draw(st.text(min_size=1, max_size=10, alphabet=st.characters(
        whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122
    )))


@st.composite
def variables(draw):
    """Generate variable identifiers (starting with 其)."""
    base = draw(identifiers())
    return f"其{base}"


@st.composite
def propositions(draw, allow_variables=True):
    """Generate propositions."""
    if allow_variables and draw(st.booleans()):
        var = draw(variables())
        return Proposition(var, True)
    else:
        ident = draw(identifiers())
        return Proposition(ident, False)


@st.composite
def svo_clauses(draw, allow_variables=True):
    """Generate SVO clauses."""
    if allow_variables:
        subj_is_var = draw(st.booleans())
        verb_is_var = draw(st.booleans())
        obj_is_var = draw(st.booleans())
    else:
        subj_is_var = verb_is_var = obj_is_var = False
    
    subj = draw(variables() if subj_is_var else identifiers())
    verb = draw(variables() if verb_is_var else identifiers())
    obj = draw(variables() if obj_is_var else identifiers())
    
    return SVOClause(subj, subj_is_var, verb, verb_is_var, obj, obj_is_var)


@st.composite
def classification_edges(draw, allow_variables=True):
    """Generate classification edges."""
    if allow_variables:
        subtype_is_var = draw(st.booleans())
        supertype_is_var = draw(st.booleans())
    else:
        subtype_is_var = supertype_is_var = False
    
    subtype = draw(variables() if subtype_is_var else identifiers())
    supertype = draw(variables() if supertype_is_var else identifiers())
    
    return ClassificationEdge(subtype, subtype_is_var, supertype, supertype_is_var)


@st.composite
def expressions(draw, allow_variables=True):
    """Generate any expression type."""
    expr_type = draw(st.sampled_from(['prop', 'svo', 'class']))
    if expr_type == 'prop':
        return draw(propositions(allow_variables=allow_variables))
    elif expr_type == 'svo':
        return draw(svo_clauses(allow_variables=allow_variables))
    else:
        return draw(classification_edges(allow_variables=allow_variables))


# Feature: ink-language-mvp, Property 41: Initial context initialization
@given(
    facts=st.lists(propositions(allow_variables=False), min_size=1, max_size=5, unique=True)
)
def test_initial_context_initialization(facts):
    """
    Property 41: Initial context initialization
    Validates: Requirements 15.1
    
    When the interpreter starts evaluation, it should initialize
    the context with asserted facts.
    """
    # Create initial context with facts
    facts_set = set(facts)
    context = Context(
        facts=facts_set.copy(),
        classification_edges=set(),
        rules=[],
        verb_lexicon=set()
    )
    
    # Run fixpoint (should not change anything without rules)
    final_context = fixpoint(context)
    
    # All initial facts should be present in final context
    for fact in facts_set:
        assert fact in final_context.facts


# Feature: ink-language-mvp, Property 42: Context growth through rules
@given(
    premise=propositions(allow_variables=False),
    conclusion=propositions(allow_variables=False)
)
def test_context_growth_through_rules(premise, conclusion):
    """
    Property 42: Context growth through rules
    Validates: Requirements 15.2
    
    When rules fire, the final context should contain both
    asserted and derived facts.
    """
    # Ensure premise and conclusion are different
    if premise == conclusion:
        return
    
    # Create a simple rule: premise → conclusion
    rule = Rule([premise], conclusion)
    
    # Create context with premise but not conclusion
    context = Context(
        facts={premise},
        classification_edges=set(),
        rules=[rule],
        verb_lexicon=set()
    )
    
    # Run fixpoint
    final_context = fixpoint(context)
    
    # Both premise and conclusion should be in final context
    assert premise in final_context.facts
    assert conclusion in final_context.facts


# Feature: ink-language-mvp, Property 43: Fixpoint termination
@given(
    num_facts=st.integers(min_value=1, max_value=5),
    num_rules=st.integers(min_value=0, max_value=3)
)
def test_fixpoint_termination(num_facts, num_rules):
    """
    Property 43: Fixpoint termination
    Validates: Requirements 15.3, 15.4
    
    For any program with finite fact space, evaluation should terminate.
    """
    # Create a set of concrete facts
    facts = {Proposition(f"事實{i}", False) for i in range(num_facts)}
    
    # Create rules that derive facts from existing facts
    rules = []
    for i in range(num_rules):
        if i < num_facts:
            premise = Proposition(f"事實{i}", False)
            conclusion = Proposition(f"推論{i}", False)
            rules.append(Rule([premise], conclusion))
    
    # Create context
    context = Context(
        facts=facts.copy(),
        classification_edges=set(),
        rules=rules,
        verb_lexicon=set()
    )
    
    # Run fixpoint - should terminate without error
    final_context = fixpoint(context)
    
    # Verify it terminated (if we get here, it terminated)
    assert isinstance(final_context, Context)


# Feature: ink-language-mvp, Property 44: Fixpoint idempotence
@given(
    facts=st.lists(propositions(allow_variables=False), min_size=1, max_size=5, unique=True)
)
def test_fixpoint_idempotence(facts):
    """
    Property 44: Fixpoint idempotence
    Validates: Requirements 15.3
    
    Running fixpoint computation twice should produce the same result
    as running it once.
    """
    # Create a simple rule that derives a new fact
    if len(facts) >= 2:
        rule = Rule([facts[0]], facts[1])
        rules = [rule]
    else:
        rules = []
    
    # Create initial context
    facts_set = set(facts)
    context = Context(
        facts=facts_set.copy(),
        classification_edges=set(),
        rules=rules,
        verb_lexicon=set()
    )
    
    # Run fixpoint once
    context1 = fixpoint(context)
    
    # Run fixpoint again on the result
    context2 = fixpoint(context1)
    
    # The facts should be identical
    assert context1.facts == context2.facts
