"""Property-based tests for the Ink parser."""

import pytest
from hypothesis import given, strategies as st, assume

from ink.parser import parse_program, ParseError
from ink.ast_nodes import (
    Proposition, SVOClause, ClassificationEdge, Rule, Query, VerbDeclaration
)


# Generators for test data
@st.composite
def identifiers(draw, allow_variable=True):
    """Generate valid Ink identifiers."""
    # Simple ASCII identifiers for testing
    base = draw(st.text(
        alphabet=st.characters(whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122),
        min_size=1,
        max_size=10
    ))
    assume(len(base) > 0)
    
    if allow_variable and draw(st.booleans()):
        return f"其{base}"
    return base


@st.composite
def verb_names(draw):
    """Generate verb names (non-variable identifiers)."""
    return draw(st.text(
        alphabet=st.characters(whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122),
        min_size=1,
        max_size=10
    ))


# Feature: ink-language-mvp, Property 7: Verb lexicon persistence
@given(
    verb1=verb_names(),
    verb2=verb_names(),
    subj=identifiers(allow_variable=False),
    obj=identifiers(allow_variable=False)
)
def test_verb_lexicon_persistence(verb1, verb2, subj, obj):
    """
    For any program with verb declarations, verbs declared in earlier statements
    should be available for SVO parsing in later statements.
    
    **Validates: Requirements 2.4**
    """
    assume(verb1 != verb2)
    assume(verb1 not in {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'})
    assume(verb2 not in {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'})
    assume(subj != verb1 and subj != verb2)
    assume(obj != verb1 and obj != verb2)
    
    # Declare verb1, use it, declare verb2, use both
    program = f"以{verb1}為動\n{subj}{verb1}{obj}\n以{verb2}為動\n{subj}{verb2}{obj}\n{subj}{verb1}{obj}"
    
    try:
        ast = parse_program(program)
        # Should have 5 statements: 2 verb decls + 3 SVO clauses
        assert len(ast) == 5
        assert isinstance(ast[0], VerbDeclaration)
        assert isinstance(ast[1], SVOClause)
        assert isinstance(ast[2], VerbDeclaration)
        assert isinstance(ast[3], SVOClause)
        assert isinstance(ast[4], SVOClause)
        # The last SVO should use verb1, showing persistence
        assert ast[4].verb == verb1
    except ParseError:
        # May fail due to other parsing issues, but not verb lexicon
        pass


# Feature: ink-language-mvp, Property 8: Verb lexicon update
@given(verb=verb_names())
def test_verb_lexicon_update(verb):
    """
    For any verb declaration 以V為動, parsing should add V to the verb lexicon.
    
    **Validates: Requirements 2.1**
    """
    assume(verb not in {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'})
    
    program = f"以{verb}為動"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], VerbDeclaration)
        assert ast[0].verb == verb
    except ParseError:
        # May fail due to parsing issues
        pass


# Feature: ink-language-mvp, Property 9: SVO segmentation with declared verbs
@given(
    verb=verb_names(),
    subj=identifiers(allow_variable=False),
    obj=identifiers(allow_variable=False)
)
def test_svo_segmentation_with_declared_verbs(verb, subj, obj):
    """
    For any SVO clause where the verb is in the lexicon, parsing should correctly
    identify subject, verb, and object positions.
    
    **Validates: Requirements 2.2, 4.1**
    """
    assume(verb not in {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'})
    assume(subj != verb and obj != verb)
    assume(subj != obj)
    # Ensure verb doesn't appear as substring in subject or object
    assume(verb not in subj)
    assume(verb not in obj)
    
    program = f"以{verb}為動\n{subj}{verb}{obj}"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 2
        assert isinstance(ast[1], SVOClause)
        assert ast[1].subject == subj
        assert ast[1].verb == verb
        assert ast[1].object == obj
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 10: SVO error on undeclared verb
@given(
    verb=verb_names(),
    subj=identifiers(allow_variable=False),
    obj=identifiers(allow_variable=False)
)
def test_svo_error_on_undeclared_verb(verb, subj, obj):
    """
    For any SVO clause where no identifier is in the verb lexicon, parsing should
    fail with an error.
    
    **Validates: Requirements 2.3, 4.2**
    """
    assume(verb not in {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'})
    assume(subj != verb and obj != verb)
    
    # Try to use verb without declaring it
    program = f"{subj}{verb}{obj}"
    
    with pytest.raises(ParseError) as exc_info:
        parse_program(program)
    
    assert "Undeclared verb" in str(exc_info.value)


# Feature: ink-language-mvp, Property 11: Proposition parsing
@given(identifier=identifiers(allow_variable=False))
def test_proposition_parsing(identifier):
    """
    For any valid identifier P, parsing 曰P should create a Proposition AST node
    with identifier P.
    
    **Validates: Requirements 3.1, 3.3**
    """
    program = f"曰{identifier}"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], Proposition)
        assert ast[0].identifier == identifier
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 12: Variable detection in propositions
@given(base=identifiers(allow_variable=False))
def test_variable_detection_in_propositions(base):
    """
    For any identifier beginning with 其, parsing 曰其X should create a Proposition
    with is_variable=True.
    
    **Validates: Requirements 3.2**
    """
    variable = f"其{base}"
    program = f"曰{variable}"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], Proposition)
        assert ast[0].is_variable is True
        assert ast[0].identifier == variable
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 13: Variable support in SVO clauses
@given(
    verb=verb_names(),
    subj=identifiers(allow_variable=True),
    obj=identifiers(allow_variable=True)
)
def test_variable_support_in_svo_clauses(verb, subj, obj):
    """
    For any SVO clause with variables in subject, verb, or object positions,
    parsing should preserve the variable flags.
    
    **Validates: Requirements 4.3**
    """
    assume(verb not in {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'})
    assume(subj != verb and obj != verb)
    # Ensure verb doesn't appear as substring in subject or object
    assume(verb not in subj)
    assume(verb not in obj)
    
    program = f"以{verb}為動\n{subj}{verb}{obj}"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 2
        assert isinstance(ast[1], SVOClause)
        
        # Check variable flags match the identifiers
        assert ast[1].subject_is_var == subj.startswith('其')
        assert ast[1].object_is_var == obj.startswith('其')
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 14: SVO order preservation
@given(
    verb=verb_names(),
    subj=identifiers(allow_variable=False),
    obj=identifiers(allow_variable=False)
)
def test_svo_order_preservation(verb, subj, obj):
    """
    For any SVO clause S V O, the resulting AST should have subject=S, verb=V,
    object=O in that order.
    
    **Validates: Requirements 4.4**
    """
    assume(verb not in {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'})
    assume(subj != verb and obj != verb and subj != obj)
    # Ensure verb doesn't appear as substring in subject or object
    assume(verb not in subj)
    assume(verb not in obj)
    
    program = f"以{verb}為動\n{subj}{verb}{obj}"
    
    try:
        ast = parse_program(program)
        if len(ast) == 2 and isinstance(ast[1], SVOClause):
            assert ast[1].subject == subj
            assert ast[1].verb == verb
            assert ast[1].object == obj
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 15: Classification parsing
@given(
    subtype=identifiers(allow_variable=False),
    supertype=identifiers(allow_variable=False)
)
def test_classification_parsing(subtype, supertype):
    """
    For any valid identifiers X and Y, parsing X者Y也 should create a
    ClassificationEdge with subtype=X and supertype=Y.
    
    **Validates: Requirements 5.1**
    """
    assume(subtype != supertype)
    
    program = f"{subtype}者{supertype}也"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], ClassificationEdge)
        assert ast[0].subtype == subtype
        assert ast[0].supertype == supertype
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 16: Variable support in classifications
@given(
    subtype=identifiers(allow_variable=True),
    supertype=identifiers(allow_variable=True)
)
def test_variable_support_in_classifications(subtype, supertype):
    """
    For any classification with variables, parsing should preserve the variable
    flags for subtype and supertype.
    
    **Validates: Requirements 5.2**
    """
    program = f"{subtype}者{supertype}也"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], ClassificationEdge)
        assert ast[0].subtype_is_var == subtype.startswith('其')
        assert ast[0].supertype_is_var == supertype.startswith('其')
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 17: Single-premise rule parsing
@given(
    premise_id=identifiers(allow_variable=False),
    conclusion_id=identifiers(allow_variable=False)
)
def test_single_premise_rule_parsing(premise_id, conclusion_id):
    """
    For any valid expressions A and B, parsing 若A則B should create a Rule with
    premises=[A] and conclusion=B.
    
    **Validates: Requirements 6.1**
    """
    program = f"若曰{premise_id}則曰{conclusion_id}"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], Rule)
        assert len(ast[0].premises) == 1
        assert isinstance(ast[0].premises[0], Proposition)
        assert isinstance(ast[0].conclusion, Proposition)
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 18: Multi-premise rule parsing
@given(
    id1=identifiers(allow_variable=False),
    id2=identifiers(allow_variable=False),
    id3=identifiers(allow_variable=False),
    id4=identifiers(allow_variable=False)
)
def test_multi_premise_rule_parsing(id1, id2, id3, id4):
    """
    For any valid expressions A, B, C, D, parsing 若A且B且C則D should create a Rule
    with premises=[A,B,C] and conclusion=D.
    
    **Validates: Requirements 7.1**
    """
    assume(len({id1, id2, id3, id4}) == 4)
    
    program = f"若曰{id1}且曰{id2}且曰{id3}則曰{id4}"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], Rule)
        assert len(ast[0].premises) == 3
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 19: Premise count flexibility
@given(
    n=st.integers(min_value=1, max_value=5),
    ids=st.lists(identifiers(allow_variable=False), min_size=6, max_size=6, unique=True)
)
def test_premise_count_flexibility(n, ids):
    """
    For any number n ≥ 1 of premises, parsing a rule with n premises should create
    a Rule with exactly n premises in the AST.
    
    **Validates: Requirements 7.2**
    """
    # Build rule with n premises
    premises_str = '且'.join(f"曰{ids[i]}" for i in range(n))
    conclusion_str = f"曰{ids[n]}"
    program = f"若{premises_str}則{conclusion_str}"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], Rule)
        assert len(ast[0].premises) == n
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 20: Premise order preservation
@given(
    id1=identifiers(allow_variable=False),
    id2=identifiers(allow_variable=False),
    id3=identifiers(allow_variable=False),
    id4=identifiers(allow_variable=False)
)
def test_premise_order_preservation(id1, id2, id3, id4):
    """
    For any multi-premise rule, the premises list in the AST should maintain
    source order.
    
    **Validates: Requirements 7.3**
    """
    assume(len({id1, id2, id3, id4}) == 4)
    
    program = f"若曰{id1}且曰{id2}且曰{id3}則曰{id4}"
    
    try:
        ast = parse_program(program)
        if len(ast) == 1 and isinstance(ast[0], Rule) and len(ast[0].premises) == 3:
            assert ast[0].premises[0].identifier == id1
            assert ast[0].premises[1].identifier == id2
            assert ast[0].premises[2].identifier == id3
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 21: Rule expression type support
@given(
    prop_id=identifiers(allow_variable=False),
    subtype=identifiers(allow_variable=False),
    supertype=identifiers(allow_variable=False)
)
def test_rule_expression_type_support(prop_id, subtype, supertype):
    """
    For any combination of propositions, SVO clauses, and classification edges as
    premises and conclusion, parsing should succeed.
    
    **Validates: Requirements 6.2, 6.3**
    """
    assume(len({prop_id, subtype, supertype}) == 3)
    
    # Mix proposition and classification
    program = f"若曰{prop_id}則{subtype}者{supertype}也"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], Rule)
        assert isinstance(ast[0].premises[0], Proposition)
        assert isinstance(ast[0].conclusion, ClassificationEdge)
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 22: Query parsing
@given(identifier=identifiers(allow_variable=False))
def test_query_parsing(identifier):
    """
    For any valid expression E, parsing 問E乎 should create a Query AST node with
    expression=E.
    
    **Validates: Requirements 8.1**
    """
    program = f"問曰{identifier}乎"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 1
        assert isinstance(ast[0], Query)
        assert isinstance(ast[0].expression, Proposition)
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 23: Query expression type support
@given(
    prop_id=identifiers(allow_variable=False),
    subtype=identifiers(allow_variable=False),
    supertype=identifiers(allow_variable=False)
)
def test_query_expression_type_support(prop_id, subtype, supertype):
    """
    For any proposition, SVO clause, or classification edge E, parsing 問E乎 should
    succeed.
    
    **Validates: Requirements 8.2**
    """
    assume(len({prop_id, subtype, supertype}) == 3)
    
    # Test with proposition
    program1 = f"問曰{prop_id}乎"
    try:
        ast = parse_program(program1)
        assert len(ast) == 1
        assert isinstance(ast[0], Query)
    except ParseError:
        pass
    
    # Test with classification
    program2 = f"問{subtype}者{supertype}也乎"
    try:
        ast = parse_program(program2)
        assert len(ast) == 1
        assert isinstance(ast[0], Query)
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 24: Program statement order preservation
@given(
    id1=identifiers(allow_variable=False),
    id2=identifiers(allow_variable=False),
    id3=identifiers(allow_variable=False)
)
def test_program_statement_order_preservation(id1, id2, id3):
    """
    For any program with multiple statements, the AST list should maintain source
    order.
    
    **Validates: Requirements 9.2**
    """
    assume(len({id1, id2, id3}) == 3)
    
    program = f"曰{id1}\n曰{id2}\n曰{id3}"
    
    try:
        ast = parse_program(program)
        assert len(ast) == 3
        assert ast[0].identifier == id1
        assert ast[1].identifier == id2
        assert ast[2].identifier == id3
    except ParseError:
        pass


# Feature: ink-language-mvp, Property 25: Program completeness
@given(
    n=st.integers(min_value=1, max_value=5),
    ids=st.lists(identifiers(allow_variable=False), min_size=5, max_size=5, unique=True)
)
def test_program_completeness(n, ids):
    """
    For any valid program with n statements, parsing should produce an AST list
    with n nodes.
    
    **Validates: Requirements 9.1**
    """
    # Build program with n propositions
    statements = [f"曰{ids[i]}" for i in range(n)]
    program = '\n'.join(statements)
    
    try:
        ast = parse_program(program)
        assert len(ast) == n
    except ParseError:
        pass
