"""
Property-based tests for the Ink language lexer.

Feature: ink-language-mvp
Tests Properties 1-6 from the design document.
"""

from hypothesis import given, strategies as st
from ink.lexer import tokenize, TokenType, LexerError


# Generators for test data

@st.composite
def valid_identifiers(draw):
    """Generate valid identifiers (Chinese characters, ASCII letters, digits, underscores)."""
    # Start with a letter or Chinese character
    first_char = draw(st.one_of(
        st.characters(min_codepoint=0x4E00, max_codepoint=0x9FFF),  # CJK Unified Ideographs
        st.characters(min_codepoint=0x3400, max_codepoint=0x4DBF),  # CJK Extension A
        st.characters(whitelist_categories=('Lu', 'Ll')),  # ASCII letters
        st.just('_')
    ))
    
    # Rest can include digits
    rest = draw(st.text(
        alphabet=st.one_of(
            st.characters(min_codepoint=0x4E00, max_codepoint=0x9FFF),
            st.characters(min_codepoint=0x3400, max_codepoint=0x4DBF),
            st.characters(whitelist_categories=('Lu', 'Ll', 'Nd')),
            st.just('_')
        ),
        min_size=0,
        max_size=20
    ))
    
    return first_char + rest


@st.composite
def valid_source_without_keywords(draw):
    """Generate valid source code without keywords to avoid lexer errors."""
    # Use only safe characters: tabs, newlines, spaces, and basic ASCII letters
    return draw(st.text(
        alphabet=st.one_of(
            st.just('\t'),
            st.just('\n'),
            st.just(' '),
            st.just('\r'),
            st.characters(whitelist_categories=('Lu', 'Ll', 'Nd')),
            st.just('_')
        ),
        min_size=0,
        max_size=100
    ))


# Property 1: Token stream completeness
# Feature: ink-language-mvp, Property 1: Token stream completeness
@given(valid_source_without_keywords())
def test_token_stream_completeness(source):
    """
    For any valid Ink source code, tokenizing should produce a token stream ending with EOF.
    
    Validates: Requirements 1.7
    """
    tokens = tokenize(source)
    assert len(tokens) > 0, "Token stream should not be empty"
    assert tokens[-1].type == TokenType.EOF, "Token stream should end with EOF"


# Property 2: Tab preservation
# Feature: ink-language-mvp, Property 2: Tab preservation
@given(st.integers(min_value=1, max_value=10))
def test_tab_preservation(num_tabs):
    """
    For any source string containing tabs, tokenizing should produce INDENT tokens at tab positions.
    
    Validates: Requirements 1.1
    """
    source = '\t' * num_tabs
    tokens = tokenize(source)
    
    # Count INDENT tokens (excluding EOF)
    indent_tokens = [t for t in tokens if t.type == TokenType.INDENT]
    assert len(indent_tokens) == num_tabs, f"Expected {num_tabs} INDENT tokens, got {len(indent_tokens)}"
    
    # Verify all INDENT tokens have correct value
    for token in indent_tokens:
        assert token.value == '\t', "INDENT token should have value '\\t'"


# Property 3: Newline preservation
# Feature: ink-language-mvp, Property 3: Newline preservation
@given(st.integers(min_value=1, max_value=10))
def test_newline_preservation(num_newlines):
    """
    For any source string containing newlines, tokenizing should produce NEWLINE tokens at newline positions.
    
    Validates: Requirements 1.2
    """
    source = '\n' * num_newlines
    tokens = tokenize(source)
    
    # Count NEWLINE tokens (excluding EOF)
    newline_tokens = [t for t in tokens if t.type == TokenType.NEWLINE]
    assert len(newline_tokens) == num_newlines, f"Expected {num_newlines} NEWLINE tokens, got {len(newline_tokens)}"
    
    # Verify all NEWLINE tokens have correct value
    for token in newline_tokens:
        assert token.value == '\n', "NEWLINE token should have value '\\n'"


# Property 4: Whitespace filtering
# Feature: ink-language-mvp, Property 4: Whitespace filtering
@given(st.integers(min_value=1, max_value=20))
def test_whitespace_filtering(num_spaces):
    """
    For any source string, tokenizing should not produce tokens for spaces or carriage returns.
    
    Validates: Requirements 1.3
    """
    # Test with spaces
    source_spaces = ' ' * num_spaces
    tokens_spaces = tokenize(source_spaces)
    
    # Should only have EOF token
    assert len(tokens_spaces) == 1, "Spaces should not produce tokens"
    assert tokens_spaces[0].type == TokenType.EOF, "Only EOF token should be present"
    
    # Test with carriage returns
    source_cr = '\r' * num_spaces
    tokens_cr = tokenize(source_cr)
    
    # Should only have EOF token
    assert len(tokens_cr) == 1, "Carriage returns should not produce tokens"
    assert tokens_cr[0].type == TokenType.EOF, "Only EOF token should be present"
    
    # Test with mixed spaces and carriage returns
    source_mixed = (' ' + '\r') * num_spaces
    tokens_mixed = tokenize(source_mixed)
    
    # Should only have EOF token
    assert len(tokens_mixed) == 1, "Spaces and carriage returns should not produce tokens"
    assert tokens_mixed[0].type == TokenType.EOF, "Only EOF token should be present"


# Property 5: Variable token recognition
# Feature: ink-language-mvp, Property 5: Variable token recognition
@given(valid_identifiers())
def test_variable_token_recognition(identifier):
    """
    For any identifier beginning with 其, tokenizing should produce a VARIABLE token.
    
    Validates: Requirements 1.5
    """
    # Skip if identifier contains keywords (lexer stops at keywords)
    keywords = {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'}
    if any(k in identifier for k in keywords):
        return
    
    source = '其' + identifier
    tokens = tokenize(source)
    
    # Should have one VARIABLE token and one EOF token
    assert len(tokens) == 2, f"Expected 2 tokens (VARIABLE + EOF), got {len(tokens)}"
    assert tokens[0].type == TokenType.VARIABLE, "First token should be VARIABLE"
    assert tokens[0].value == source, f"VARIABLE token value should be '{source}'"
    assert tokens[1].type == TokenType.EOF, "Second token should be EOF"


# Property 6: Identifier token recognition
# Feature: ink-language-mvp, Property 6: Identifier token recognition
@given(valid_identifiers())
def test_identifier_token_recognition(identifier):
    """
    For any valid identifier not beginning with 其 and not a keyword, tokenizing should produce an IDENTIFIER token.
    
    Validates: Requirements 1.6
    """
    # Ensure the identifier doesn't start with 其 and isn't a keyword
    keywords = {'曰', '問', '乎', '若', '則', '且', '者', '也', '以', '為', '動'}
    
    # Skip if identifier is a keyword or starts with a keyword character
    if identifier in keywords or any(identifier.startswith(k) for k in keywords):
        return
    
    # Skip if identifier starts with 其
    if identifier.startswith('其'):
        return
    
    # Skip if identifier contains keywords (lexer stops at keywords)
    if any(k in identifier for k in keywords):
        return
    
    tokens = tokenize(identifier)
    
    # Should have one IDENTIFIER token and one EOF token
    assert len(tokens) == 2, f"Expected 2 tokens (IDENTIFIER + EOF), got {len(tokens)}"
    assert tokens[0].type == TokenType.IDENTIFIER, "First token should be IDENTIFIER"
    assert tokens[0].value == identifier, f"IDENTIFIER token value should be '{identifier}'"
    assert tokens[1].type == TokenType.EOF, "Second token should be EOF"
