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


# 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 37: Rule firing on satisfied premises
@given(
    conclusion=expressions(allow_variables=False),
    num_premises=st.integers(min_value=1, max_value=3)
)
def test_rule_fires_on_satisfied_premises(conclusion, num_premises):
    """
    Property 37: Rule firing on satisfied premises
    Validates: Requirements 14.1
    
    When all premises of a rule unify with facts in the context,
    the rule should derive the conclusion with substitutions applied.
    """
    # Create concrete premises (no variables)
    premises = []
    for _ in range(num_premises):
        if isinstance(conclusion, Proposition):
            prem = Proposition(f"前提{_}", False)
        elif isinstance(conclusion, SVOClause):
            prem = SVOClause(f"主{_}", False, f"動{_}", False, f"賓{_}", False)
        else:
            prem = ClassificationEdge(f"子{_}", False, f"父{_}", False)
        premises.append(prem)
    
    # Create rule
    rule = Rule(premises, conclusion)
    
    # Create context with all premises as facts
    context = Context(
        facts=set(premises),
        classification_edges=set(),
        rules=[],
        verb_lexicon=set()
    )
    
    # Apply rule
    derived = apply_rule(rule, context)
    
    # The conclusion should be derived
    assert conclusion in derived


# Feature: ink-language-mvp, Property 38: Rule non-firing on unsatisfied premises
@given(
    conclusion=expressions(allow_variables=False),
    num_premises=st.integers(min_value=2, max_value=3)
)
def test_rule_does_not_fire_on_unsatisfied_premises(conclusion, num_premises):
    """
    Property 38: Rule non-firing on unsatisfied premises
    Validates: Requirements 14.2
    
    When any premise fails to unify with context facts,
    the rule should not derive any conclusion.
    """
    # Create concrete premises
    premises = []
    for i in range(num_premises):
        if isinstance(conclusion, Proposition):
            prem = Proposition(f"前提{i}", False)
        elif isinstance(conclusion, SVOClause):
            prem = SVOClause(f"主{i}", False, f"動{i}", False, f"賓{i}", False)
        else:
            prem = ClassificationEdge(f"子{i}", False, f"父{i}", False)
        premises.append(prem)
    
    # Create rule
    rule = Rule(premises, conclusion)
    
    # Create context with only SOME premises (not all)
    # This ensures at least one premise is unsatisfied
    context = Context(
        facts=set(premises[:-1]),  # Missing the last premise
        classification_edges=set(),
        rules=[],
        verb_lexicon=set()
    )
    
    # Apply rule
    derived = apply_rule(rule, context)
    
    # No facts should be derived
    assert len(derived) == 0


# Feature: ink-language-mvp, Property 39: Exhaustive variable binding
@given(
    var_name=variables(),
    concrete_values=st.lists(identifiers(), min_size=2, max_size=4, unique=True)
)
def test_exhaustive_variable_binding(var_name, concrete_values):
    """
    Property 39: Exhaustive variable binding
    Validates: Requirements 14.3
    
    The interpreter should try all possible substitutions for rules with variables.
    """
    # Create a rule: 若其X則其X (if X then X)
    # This rule should fire for each concrete value in the context
    premise = Proposition(var_name, True)
    conclusion = Proposition(var_name, True)
    rule = Rule([premise], conclusion)
    
    # Create context with multiple concrete propositions
    facts = {Proposition(val, False) for val in concrete_values}
    context = Context(
        facts=facts,
        classification_edges=set(),
        rules=[],
        verb_lexicon=set()
    )
    
    # Apply rule
    derived = apply_rule(rule, context)
    
    # Should derive a fact for each concrete value
    # (though they'll be the same as the premises in this case)
    # The key is that the rule tried all bindings
    assert len(derived) == 0  # All facts already in context (deduplication)


# Feature: ink-language-mvp, Property 40: Fact deduplication
@given(
    fact=expressions(allow_variables=False)
)
def test_fact_deduplication(fact):
    """
    Property 40: Fact deduplication
    Validates: Requirements 14.4
    
    The interpreter should not add duplicate facts to the context.
    """
    # Create a rule that derives a fact already in the context
    premise = Proposition("前提", False)
    rule = Rule([premise], fact)
    
    # Create context with both premise and conclusion already present
    context = Context(
        facts={premise, fact},
        classification_edges=set(),
        rules=[],
        verb_lexicon=set()
    )
    
    # Apply rule
    derived = apply_rule(rule, context)
    
    # Should not derive the fact again (it's already in context)
    assert fact not in derived
    assert len(derived) == 0


# Feature: ink-language-mvp, Property 36: Derived edges in rule matching
@given(
    middle=identifiers(),
    start=identifiers(),
    end=identifiers()
)
def test_derived_edges_in_rule_matching(start, middle, end):
    """
    Property 36: Derived edges in rule matching
    Validates: Requirements 13.3
    
    Rules should match against both asserted and derived classification edges.
    """
    # Ensure all identifiers are different
    if start == middle or middle == end or start == end:
        return
    
    # Create classification edges: start⊑middle and middle⊑end
    edge1 = ClassificationEdge(start, False, middle, False)
    edge2 = ClassificationEdge(middle, False, end, False)
    
    # Create a rule that matches on the derived transitive edge
    # 若start⊑end則result
    derived_edge = ClassificationEdge(start, False, end, False)
    result = Proposition("結果", False)
    rule = Rule([derived_edge], result)
    
    # Create context with only the two base edges
    classification_edges = {edge1, edge2}
    
    # Compute closure to get the derived edge
    closed_edges = compute_classification_closure(classification_edges)
    
    context = Context(
        facts=closed_edges,
        classification_edges=closed_edges,
        rules=[],
        verb_lexicon=set()
    )
    
    # Apply rule
    derived_facts = apply_rule(rule, context)
    
    # The rule should fire because the derived edge is in the context
    assert result in derived_facts
