# tests/test_comprehensive_math_verifier.py
import pytest
from llm_guard.verification.comprehensive_math_verifier import ComprehensiveMathVerifier

@pytest.fixture
def v():
    return ComprehensiveMathVerifier()

# --- Answer parser ---
def test_parse_plain_integer(v):
    assert v._parse_answer("7") == pytest.approx(7.0)

def test_parse_negative(v):
    assert v._parse_answer("-15") == pytest.approx(-15.0)

def test_parse_decimal(v):
    assert v._parse_answer("3.14") == pytest.approx(3.14)

def test_parse_fraction_latex(v):
    assert v._parse_answer(r"\frac{11}{2}") == pytest.approx(5.5)

def test_parse_negative_fraction_latex(v):
    assert v._parse_answer(r"-\frac{1}{8}") == pytest.approx(-0.125)

def test_parse_thousand_separator(v):
    assert v._parse_answer("1,000") == pytest.approx(1000.0)

def test_parse_tuple_returns_none(v):
    assert v._parse_answer("4,6,14,15") is None

def test_parse_expression_returns_none(v):
    assert v._parse_answer("x^2 + 1") is None

def test_parse_sqrt_latex(v):
    # \sqrt{3} -> ~1.732
    result = v._parse_answer(r"\sqrt{3}")
    assert result is not None
    assert abs(result - 1.7320508) < 1e-4

# --- Problem classifier ---
def test_classify_number_theory(v):
    assert v._classify("Is 17 prime?") == "number_theory"

def test_classify_combinatorics(v):
    assert v._classify("How many ways to choose 3 from 10?") == "combinatorics"

def test_classify_geometry(v):
    assert v._classify("Find the area of a circle with radius 5.") == "geometry"

def test_classify_finance(v):
    assert v._classify("What is the compound interest on $1000 at 5% annual rate?") == "finance"

def test_classify_algebra_default(v):
    assert v._classify("Solve x^2 - 5x + 6 = 0") == "algebra"

def test_classify_proof(v):
    assert v._classify("Prove that n^2 + n is always even for all positive integers n.") == "proof"

# --- score() returns neutral when unverifiable ---
def test_score_unverifiable_returns_half(v):
    risk = v.score("What colour is the sky?", [], "blue")
    assert risk == pytest.approx(0.5)

# --- Algebra verifier ---
def test_algebra_linear_correct(v):
    # 2x + 3 = 11 → x = 4
    risk = v.score("Solve 2x + 3 = 11 for x.", [], "4")
    assert risk == pytest.approx(0.10)

def test_algebra_linear_wrong(v):
    risk = v.score("Solve 2x + 3 = 11 for x.", [], "5")
    assert risk == pytest.approx(0.90)

def test_algebra_quadratic_correct(v):
    # x^2 - 5x + 6 = 0 → x = 2 or x = 3
    risk = v.score("Find the roots of x^2 - 5x + 6 = 0. Give the smaller root.", [], "2")
    assert risk in (pytest.approx(0.10), pytest.approx(0.50))  # 0.50 acceptable if multi-root

def test_algebra_absolute_value_correct(v):
    # |5x - 1| = |3x + 2| → x = 3/2 or x = -1/8
    risk = v.score(
        r"What is the smallest value of $x$ such that $|5x - 1| = |3x + 2|$?",
        [], "-0.125"
    )
    assert risk == pytest.approx(0.10)

def test_algebra_absolute_value_wrong(v):
    risk = v.score(
        r"What is the smallest value of $x$ such that $|5x - 1| = |3x + 2|$?",
        [], "8"
    )
    assert risk == pytest.approx(0.90)

def test_algebra_unextractable_returns_half(v):
    # Can't extract equation
    risk = v.score("Simplify the expression $a^2 + 2ab + b^2$.", [], "25")
    assert risk == pytest.approx(0.50)

# --- Number theory verifier ---
def test_nt_primality_correct(v):
    # Is 17 prime? → yes → proposed answer 1 (true)
    risk = v.score("Is 17 a prime number? Answer 1 for yes, 0 for no.", [], "1")
    assert risk == pytest.approx(0.10)

def test_nt_primality_wrong(v):
    risk = v.score("Is 15 a prime number? Answer 1 for yes, 0 for no.", [], "1")
    assert risk == pytest.approx(0.90)

def test_nt_gcd_correct(v):
    risk = v.score("Find the GCD of 48 and 36.", [], "12")
    assert risk == pytest.approx(0.10)

def test_nt_gcd_wrong(v):
    risk = v.score("Find the GCD of 48 and 36.", [], "6")
    assert risk == pytest.approx(0.90)

def test_nt_lcm_correct(v):
    risk = v.score("Find the LCM of 4 and 6.", [], "12")
    assert risk == pytest.approx(0.10)

def test_nt_mod_correct(v):
    risk = v.score("What is 17 mod 5?", [], "2")
    assert risk == pytest.approx(0.10)

def test_nt_mod_wrong(v):
    risk = v.score("What is 17 mod 5?", [], "3")
    assert risk == pytest.approx(0.90)

def test_nt_count_primes_correct(v):
    # Primes less than 10: 2,3,5,7 → 4
    risk = v.score("How many prime numbers are less than 10?", [], "4")
    assert risk == pytest.approx(0.10)

# --- Combinatorics verifier ---
def test_comb_choose_correct(v):
    # C(10, 3) = 120
    risk = v.score("How many ways to choose 3 items from 10?", [], "120")
    assert risk == pytest.approx(0.10)

def test_comb_choose_wrong(v):
    risk = v.score("How many ways to choose 3 items from 10?", [], "30")
    assert risk == pytest.approx(0.90)

def test_comb_permutation_correct(v):
    # P(5,3) = 60
    risk = v.score("How many permutations of 3 items from 5 are there?", [], "60")
    assert risk == pytest.approx(0.10)

def test_comb_factorial_correct(v):
    # 5! = 120
    risk = v.score("How many ways can 5 distinct books be arranged on a shelf?", [], "120")
    assert risk == pytest.approx(0.10)

def test_comb_probability_correct(v):
    # P(heads) = 1/2 = 0.5
    risk = v.score("A fair coin is flipped. What is the probability of heads?", [], "0.5")
    assert risk == pytest.approx(0.10)

def test_comb_probability_wrong(v):
    risk = v.score("A fair coin is flipped. What is the probability of heads?", [], "0.25")
    assert risk == pytest.approx(0.90)

def test_comb_large_choose_correct(v):
    # C(100, 2) = 4950 — large n should not overflow (use math.comb)
    risk = v.score("How many ways to choose 2 items from 100?", [], "4950")
    assert risk == pytest.approx(0.10)

def test_comb_k_greater_than_n_unverifiable(v):
    # k > n is impossible — should be unverifiable, not crash
    risk = v.score("How many ways to choose 10 items from 3?", [], "0")
    assert risk == pytest.approx(0.50)

# --- Geometry verifier ---
def test_geom_circle_area_correct(v):
    # π * 5^2 ≈ 78.54
    risk = v.score("Find the area of a circle with radius 5.", [], "78.54")
    assert risk == pytest.approx(0.10)

def test_geom_circle_area_wrong(v):
    risk = v.score("Find the area of a circle with radius 5.", [], "31.4")
    assert risk == pytest.approx(0.90)

def test_geom_circle_circumference_correct(v):
    # 2π*5 ≈ 31.416
    risk = v.score("Find the circumference of a circle with radius 5.", [], "31.416")
    assert risk == pytest.approx(0.10)

def test_geom_rectangle_area_correct(v):
    risk = v.score("Find the area of a rectangle with length 8 and width 3.", [], "24")
    assert risk == pytest.approx(0.10)

def test_geom_pythagorean_correct(v):
    # 3-4-5 right triangle
    risk = v.score(
        "A right triangle has legs of length 3 and 4. Find the hypotenuse.", [], "5"
    )
    assert risk == pytest.approx(0.10)

def test_geom_pythagorean_wrong(v):
    risk = v.score(
        "A right triangle has legs of length 3 and 4. Find the hypotenuse.", [], "7"
    )
    assert risk == pytest.approx(0.90)

def test_geom_distance_correct(v):
    # distance (0,0) to (3,4) = 5
    risk = v.score(
        "Find the distance between points (0, 0) and (3, 4).", [], "5"
    )
    assert risk == pytest.approx(0.10)

def test_geom_circle_zero_radius(v):
    # radius=0: area=0 is correct
    risk = v.score("Find the area of a circle with radius 0.", [], "0")
    assert risk == pytest.approx(0.10)

def test_geom_rectangle_negative_inputs_unverifiable(v):
    # negative side lengths don't make geometric sense — should be unverifiable
    risk = v.score("Find the area of a rectangle with length -3 and width 4.", [], "12")
    assert risk == pytest.approx(0.50)

# --- Finance verifier ---
def test_finance_compound_interest_correct(v):
    # P=1000, r=0.05, n=1, t=2 → A = 1000*(1.05)^2 = 1102.50
    risk = v.score(
        "What is the compound interest on $1000 at an annual rate of 5% "
        "compounded annually for 2 years?",
        [], "1102.50"
    )
    assert risk == pytest.approx(0.10)

def test_finance_compound_interest_wrong(v):
    risk = v.score(
        "What is the compound interest on $1000 at an annual rate of 5% "
        "compounded annually for 2 years?",
        [], "1100.0"
    )
    assert risk == pytest.approx(0.90)

def test_finance_simple_interest_correct(v):
    # P=1000, r=0.05, t=3 → A = 1000*(1 + 0.05*3) = 1150
    risk = v.score(
        "Find the simple interest on a principal of $1000 at 5% annual rate for 3 years.",
        [], "1150"
    )
    assert risk == pytest.approx(0.10)

def test_finance_pct_change_correct(v):
    # (120 - 100) / 100 * 100 = 20%
    risk = v.score(
        "A stock price increased from $100 to $120. What is the percentage change?",
        [], "20"
    )
    assert risk == pytest.approx(0.10)

# --- Proof sampler ---
def test_proof_identity_correct(v):
    # n^2 + n is always even — proposed "1" means proved
    risk = v.score(
        "Prove that n^2 + n is always even for all positive integers n. "
        "Answer 1 if true, 0 if false.",
        [], "1"
    )
    # Either 0.10 (verified) or 0.50 (unverifiable) — NOT 0.90
    assert risk in (pytest.approx(0.10), pytest.approx(0.50))

def test_proof_identity_counterexample(v):
    # "n^2 is always odd" — FALSE, counterexample n=2
    risk = v.score(
        "Is it true that n^2 is always odd for all positive integers n? Answer 1 if true, 0 if false.",
        [], "1"
    )
    # Should flag as wrong (counterexample: n=2 gives 4, which is even)
    # Or unverifiable if pattern not extracted
    assert risk in (pytest.approx(0.90), pytest.approx(0.50))

# --- Graph theory verifier ---
def test_graph_degree_sum_correct(v):
    # Vertices with degrees 2,3,3,4 → sum=12, edges=6
    risk = v.score(
        "A graph has vertices with degrees 2, 3, 3, and 4. How many edges does it have?",
        [], "6"
    )
    assert risk == pytest.approx(0.10)

def test_graph_degree_sum_wrong(v):
    risk = v.score(
        "A graph has vertices with degrees 2, 3, 3, and 4. How many edges does it have?",
        [], "5"
    )
    assert risk == pytest.approx(0.90)
