from hypothesis import given, strategies as st
from ink.ast_nodes import Proposition, SVOClause, ClassificationEdge
from ink.interpreter import unify, apply_substitution


# Generators for test data
def identifiers():
    """Generate valid identifiers (non-variable)."""
    return st.text(min_size=1, max_size=10, alphabet=st.characters(
        whitelist_categories=('Lu', 'Ll', 'Lo'),
        blacklist_characters='其'
    ))


def variable_identifiers():
    """Generate variable identifiers (starting with 其)."""
    return st.builds(lambda s: f"其{s}", st.text(min_size=1, max_size=10, alphabet=st.characters(
        whitelist_categories=('Lu', 'Ll', 'Lo')
    )))


def propositions(allow_variables=True):
    """Generate propositions."""
    if allow_variables:
        return st.one_of(
            st.builds(Proposition, identifiers(), st.just(False)),
            st.builds(Proposition, variable_identifiers(), st.just(True))
        )
    else:
        return st.builds(Proposition, identifiers(), st.just(False))


def svo_clauses(allow_variables=True):
    """Generate SVO clauses."""
    if allow_variables:
        return st.builds(
            SVOClause,
            st.one_of(identifiers(), variable_identifiers()),
            st.booleans(),
            st.one_of(identifiers(), variable_identifiers()),
            st.booleans(),
            st.one_of(identifiers(), variable_identifiers()),
            st.booleans()
        )
    else:
        return st.builds(
            SVOClause,
            identifiers(),
            st.just(False),
            identifiers(),
            st.just(False),
            identifiers(),
            st.just(False)
        )


def classification_edges(allow_variables=True):
    """Generate classification edges."""
    if allow_variables:
        return st.builds(
            ClassificationEdge,
            st.one_of(identifiers(), variable_identifiers()),
            st.booleans(),
            st.one_of(identifiers(), variable_identifiers()),
            st.booleans()
        )
    else:
        return st.builds(
            ClassificationEdge,
            identifiers(),
            st.just(False),
            identifiers(),
            st.just(False)
        )


# Feature: ink-language-mvp, Property 26: Proposition identity unification
@given(identifiers())
def test_proposition_identity_unification(identifier):
    """For any proposition P, unifying P with P should succeed with empty substitution."""
    prop = Proposition(identifier, False)
    result = unify(prop, prop, {})
    assert result == {}


# Feature: ink-language-mvp, Property 27: Proposition variable binding
@given(variable_identifiers(), identifiers())
def test_proposition_variable_binding(var_id, concrete_id):
    """For any variable 其X and concrete proposition P, unifying should bind the variable."""
    var_prop = Proposition(var_id, True)
    concrete_prop = Proposition(concrete_id, False)
    result = unify(var_prop, concrete_prop, {})
    assert result is not None
    assert result[var_id] == concrete_id


# Feature: ink-language-mvp, Property 28: Proposition unification failure
@given(identifiers(), identifiers())
def test_proposition_unification_failure(id1, id2):
    """For any two different concrete propositions, unification should fail."""
    if id1 == id2:
        return  # Skip identical propositions
    prop1 = Proposition(id1, False)
    prop2 = Proposition(id2, False)
    result = unify(prop1, prop2, {})
    assert result is None


# Feature: ink-language-mvp, Property 29: Substitution application before unification
@given(variable_identifiers(), identifiers(), identifiers())
def test_substitution_application_before_unification(var_id, binding, concrete_id):
    """Existing substitutions should be applied before unification."""
    var_prop = Proposition(var_id, True)
    concrete_prop = Proposition(concrete_id, False)
    existing_subst = {var_id: binding}
    
    result = unify(var_prop, concrete_prop, existing_subst)
    
    if binding == concrete_id:
        # Should succeed since variable is already bound to the same value
        assert result == existing_subst
    else:
        # Should fail since variable is bound to a different value
        assert result is None


# Feature: ink-language-mvp, Property 30: SVO compositional unification
@given(
    identifiers(), identifiers(), identifiers(),
    identifiers(), identifiers(), identifiers()
)
def test_svo_compositional_unification(s1, v1, o1, s2, v2, o2):
    """SVO unification should succeed iff subject, verb, and object all unify."""
    svo1 = SVOClause(s1, False, v1, False, o1, False)
    svo2 = SVOClause(s2, False, v2, False, o2, False)
    
    result = unify(svo1, svo2, {})
    
    if s1 == s2 and v1 == v2 and o1 == o2:
        assert result == {}
    else:
        assert result is None


# Feature: ink-language-mvp, Property 31: SVO variable position support
@given(
    st.one_of(identifiers(), variable_identifiers()),
    st.booleans(),
    identifiers(),
    identifiers(),
    identifiers()
)
def test_svo_variable_position_support(subj, subj_is_var, verb, obj, concrete_subj):
    """SVO clauses with variables in any position should unify correctly."""
    pattern = SVOClause(subj, subj_is_var, verb, False, obj, False)
    fact = SVOClause(concrete_subj, False, verb, False, obj, False)
    
    result = unify(pattern, fact, {})
    
    if subj_is_var:
        # Variable should be bound
        assert result is not None
        assert result[subj] == concrete_subj
    else:
        # Concrete values should match
        if subj == concrete_subj:
            assert result == {}
        else:
            assert result is None


# Feature: ink-language-mvp, Property 32: Classification compositional unification
@given(
    identifiers(), identifiers(),
    identifiers(), identifiers()
)
def test_classification_compositional_unification(sub1, sup1, sub2, sup2):
    """Classification unification should succeed iff subtype and supertype both unify."""
    edge1 = ClassificationEdge(sub1, False, sup1, False)
    edge2 = ClassificationEdge(sub2, False, sup2, False)
    
    result = unify(edge1, edge2, {})
    
    if sub1 == sub2 and sup1 == sup2:
        assert result == {}
    else:
        assert result is None


# Feature: ink-language-mvp, Property 33: Classification variable position support
@given(
    st.one_of(identifiers(), variable_identifiers()),
    st.booleans(),
    identifiers(),
    identifiers()
)
def test_classification_variable_position_support(subtype, subtype_is_var, supertype, concrete_subtype):
    """Classification edges with variables should unify correctly."""
    pattern = ClassificationEdge(subtype, subtype_is_var, supertype, False)
    fact = ClassificationEdge(concrete_subtype, False, supertype, False)
    
    result = unify(pattern, fact, {})
    
    if subtype_is_var:
        # Variable should be bound
        assert result is not None
        assert result[subtype] == concrete_subtype
    else:
        # Concrete values should match
        if subtype == concrete_subtype:
            assert result == {}
        else:
            assert result is None
