"""DSL 解析器单测。覆盖正例 + 反例。"""

from __future__ import annotations

import pytest

from xiangqin.dsl import DSLError, parse


# --- 正例 ---


def test_single_gender():
    p = parse("gender=f")
    assert p.sql == "gender = ?"
    assert p.params == ["f"]


def test_age_range():
    p = parse("age>=25 AND age<=30")
    assert p.sql == "age >= ? AND age <= ?"
    assert p.params == [25, 30]


def test_city_exact():
    p = parse("city=hangzhou")
    assert p.sql == "city = ?"
    assert p.params == ["hangzhou"]


def test_city_chinese():
    p = parse("city=杭州")
    assert p.params == ["杭州"]


def test_tags_contains():
    p = parse("tags CONTAINS 登山")
    assert p.sql == "tags LIKE ?"
    assert p.params == ["%|登山|%"]


def test_tags_case_insensitive():
    p = parse("tags contains 登山")
    assert p.sql == "tags LIKE ?"


def test_combined():
    p = parse("gender=m AND city=hangzhou AND age>=25 AND age<=35 AND tags CONTAINS 登山")
    assert p.sql == ("gender = ? AND city = ? AND age >= ? AND age <= ? AND tags LIKE ?")
    assert p.params == ["m", "hangzhou", 25, 35, "%|登山|%"]


def test_gender_not_equal():
    p = parse("gender!=f")
    assert p.sql == "gender != ?"
    assert p.params == ["f"]


# --- 反例：结构 ---


def test_empty():
    with pytest.raises(DSLError) as e:
        parse("")
    assert e.value.code == "dsl_empty"


def test_none():
    with pytest.raises(DSLError) as e:
        parse("   ")
    assert e.value.code == "dsl_empty"


def test_or_not_allowed():
    with pytest.raises(DSLError) as e:
        parse("gender=f OR gender=m")
    assert e.value.code == "dsl_no_or"


def test_forbidden_char_semicolon():
    with pytest.raises(DSLError) as e:
        parse("gender=f;")
    assert e.value.code == "dsl_forbidden_char"


def test_forbidden_char_quote():
    with pytest.raises(DSLError) as e:
        parse("city='hangzhou'")
    assert e.value.code == "dsl_forbidden_char"


def test_sql_injection_attempt():
    with pytest.raises(DSLError) as e:
        parse("gender=f AND 1=1 UNION SELECT * FROM users")
    assert e.value.code in {"dsl_forbidden_keyword", "dsl_bad_clause"}


def test_forbidden_keyword_drop():
    with pytest.raises(DSLError) as e:
        parse("gender=f AND DROP TABLE users")
    assert e.value.code == "dsl_forbidden_keyword"


def test_too_many_clauses():
    expr = " AND ".join(["gender=f"] * 9)
    with pytest.raises(DSLError) as e:
        parse(expr)
    assert e.value.code == "dsl_too_many_clauses"


# --- 反例：字段 / 操作符 / 值 ---


def test_unknown_field():
    with pytest.raises(DSLError) as e:
        parse("income>5000")
    assert e.value.code == "dsl_bad_clause"


def test_gender_bad_op():
    with pytest.raises(DSLError) as e:
        parse("gender>f")
    assert e.value.code == "dsl_bad_op"


def test_age_below_min():
    with pytest.raises(DSLError) as e:
        parse("age=15")
    assert e.value.code == "dsl_bad_value"


def test_age_not_integer():
    with pytest.raises(DSLError) as e:
        parse("age=abc")
    assert e.value.code == "dsl_bad_value"


def test_gender_bad_value():
    with pytest.raises(DSLError) as e:
        parse("gender=x")
    assert e.value.code == "dsl_bad_value"


def test_city_illegal_char():
    with pytest.raises(DSLError) as e:
        parse("city=hang/zhou")
    assert e.value.code == "dsl_bad_value"


def test_tag_too_long():
    long_tag = "a" * 25
    with pytest.raises(DSLError) as e:
        parse(f"tags CONTAINS {long_tag}")
    assert e.value.code == "dsl_bad_value"
