"""algebra-symbolic@v1 π* tests (ticket #000030 Phase 1).

Covers polynomial-identity equivalence, distinct-class separation,
round-trip idempotence, error paths, and the optional-extra skip
behavior. Whole file skips when SymPy is absent.
"""

from __future__ import annotations

import pytest

sympy = pytest.importorskip("sympy")  # entire file skipped if absent

from arborist.pi_star import get  # noqa: E402
from arborist.pi_star.protocol import (  # noqa: E402
    PiStarError,
    assert_round_trip,
    equivalence_class_id,
)


@pytest.fixture
def ps():
    return get("algebra-symbolic@v1")


# --- registry presence ------------------------------------------------


def test_registry_contains_algebra_symbolic():
    ps = get("algebra-symbolic@v1")
    assert ps.name == "algebra-symbolic"
    assert ps.version == "v1"
    assert ps.domain == "symbolic-algebra"


# --- polynomial-identity equivalence classes -------------------------


@pytest.mark.parametrize(
    "left,right",
    [
        ("(x+1)**2", "x**2 + 2*x + 1"),
        ("(x+1)**2", "x*x + 2*x + 1"),
        ("(a+b)*(a-b)", "a**2 - b**2"),
        ("2*x + 3*x", "5*x"),
        ("x*(x+1)", "x**2 + x"),
        ("(x-1)*(x-2)", "x**2 - 3*x + 2"),
    ],
)
def test_polynomial_identity_collapses(ps, left, right):
    assert ps.canonicalize(left.encode()) == ps.canonicalize(right.encode())
    assert (
        equivalence_class_id(ps, left.encode())
        == equivalence_class_id(ps, right.encode())
    )


@pytest.mark.parametrize(
    "left,right",
    [
        ("x**2", "x**3"),
        ("x + 1", "x"),
        ("(x+1)**2", "(x+1)**3"),
        ("a*b", "a + b"),
    ],
)
def test_distinct_polynomials_distinct(ps, left, right):
    assert ps.canonicalize(left.encode()) != ps.canonicalize(right.encode())


# --- round-trip / idempotence ----------------------------------------


@pytest.mark.parametrize(
    "expr",
    [
        "x",
        "x + 1",
        "(x+1)**2",
        "a*b - b*a",  # zero
        "x**2 - 2*x + 1",
        "(a+b)*(c+d)",
        "1/2 + 1/3",  # purely numeric — π* still accepts
    ],
)
def test_round_trip_idempotent(ps, expr):
    assert_round_trip(ps, expr.encode())


# --- canonical bytes shape -------------------------------------------


def test_canonical_bytes_are_srepr_form(ps):
    out = ps.canonicalize(b"(x+1)**2").decode()
    # srepr emits S-expression syntax with capitalized SymPy class names.
    assert out.startswith("Add(")
    assert "Pow(Symbol('x'), Integer(2))" in out


def test_numeric_inputs_produce_pure_numeric_canonical(ps):
    # 1/2 + 1/3 = 5/6
    out = ps.canonicalize(b"1/2 + 1/3").decode()
    assert out == "Rational(5, 6)"


# --- error paths ------------------------------------------------------


def test_empty_input_raises(ps):
    with pytest.raises(PiStarError, match="empty"):
        ps.canonicalize(b"")
    with pytest.raises(PiStarError, match="empty"):
        ps.canonicalize(b"   \n\t  ")


def test_unparseable_input_raises(ps):
    with pytest.raises(PiStarError, match="cannot parse"):
        ps.canonicalize(b"not a valid expression $$$ %%%")


def test_non_bytes_input_raises(ps):
    with pytest.raises(PiStarError, match="expects bytes"):
        ps.canonicalize("not bytes")  # type: ignore[arg-type]


def test_relational_input_rejected(ps):
    with pytest.raises(PiStarError, match="boolean/relational"):
        ps.canonicalize(b"x > 0")


# --- known limitation: trig identity NOT collapsed by Phase 1 --------


def test_trig_identity_not_collapsed_in_phase_1(ps):
    """Documents the known Phase-1 limitation. Phase 1b adds a
    `algebra-symbolic-simplified@v1` variant that uses ``sp.simplify``
    at unbounded CPU cost.
    """
    a = ps.canonicalize(b"sin(x)**2 + cos(x)**2")
    b = ps.canonicalize(b"1")
    assert a != b


# --- composition with other π* ----------------------------------------


def test_compose_with_arithmetic_for_numeric_eval():
    """algebra-symbolic@v1 ∘ arithmetic@v1 — symbolic identity that
    happens to reduce to a number passes through both stages."""
    from arborist.pi_star import compose

    chain = compose("algebra-symbolic@v1", "arithmetic@v1")
    # Bridge: take a symbolic answer that reduces to pure number,
    # round-trip its srepr form into arithmetic? No — composition is
    # just chained canonicalize. We test the chain registers cleanly.
    assert chain is not None
    # Instead exercise pure-numeric flow that BOTH π*'s can handle:
    # algebra-symbolic@v1 accepts `1/2 + 1/3` and emits Rational(5, 6);
    # composition itself runs the SECOND π* on the FIRST's bytes — and
    # arithmetic@v1 cannot parse Rational(5, 6). We expect PiStarError
    # in that direction. Document the failure as the natural composition
    # boundary.
    with pytest.raises(PiStarError):
        chain.canonicalize(b"1/2 + 1/3")
