"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

import pytest

from control_backend.agents.bdi.agentspeak_ast import (
    AstAtom,
    AstBinaryOp,
    AstLiteral,
    AstLogicalExpression,
    AstNumber,
    AstPlan,
    AstProgram,
    AstRule,
    AstStatement,
    AstString,
    AstVar,
    BinaryOperatorType,
    StatementType,
    TriggerType,
    _coalesce_expr,
)


def test_ast_atom():
    atom = AstAtom("test")
    assert str(atom) == "test"
    assert atom._to_agentspeak() == "test"


def test_ast_var():
    var = AstVar("Variable")
    assert str(var) == "Variable"
    assert var._to_agentspeak() == "Variable"


def test_ast_number():
    num = AstNumber(42)
    assert str(num) == "42"
    num_float = AstNumber(3.14)
    assert str(num_float) == "3.14"


def test_ast_string():
    s = AstString("hello")
    assert str(s) == '"hello"'


def test_ast_literal():
    lit = AstLiteral("functor", [AstAtom("atom"), AstNumber(1)])
    assert str(lit) == "functor(atom, 1)"
    lit_empty = AstLiteral("functor")
    assert str(lit_empty) == "functor"


def test_ast_binary_op():
    left = AstNumber(1)
    right = AstNumber(2)
    op = AstBinaryOp(left, BinaryOperatorType.GREATER_THAN, right)
    assert str(op) == "1 > 2"

    # Test logical wrapper
    assert isinstance(op.left, AstLogicalExpression)
    assert isinstance(op.right, AstLogicalExpression)


def test_ast_binary_op_parens():
    # 1 > 2
    inner = AstBinaryOp(AstNumber(1), BinaryOperatorType.GREATER_THAN, AstNumber(2))
    # (1 > 2) & 3
    outer = AstBinaryOp(inner, BinaryOperatorType.AND, AstNumber(3))
    assert str(outer) == "(1 > 2) & 3"

    # 3 & (1 > 2)
    outer_right = AstBinaryOp(AstNumber(3), BinaryOperatorType.AND, inner)
    assert str(outer_right) == "3 & (1 > 2)"


def test_ast_binary_op_parens_negated():
    inner = AstLogicalExpression(AstAtom("foo"), negated=True)
    outer = AstBinaryOp(inner, BinaryOperatorType.AND, AstAtom("bar"))
    # The current implementation checks `if self.left.negated: l_str = f"({l_str})"`
    # str(inner) is "not foo"
    # so we expect "(not foo) & bar"
    assert str(outer) == "(not foo) & bar"

    outer_right = AstBinaryOp(AstAtom("bar"), BinaryOperatorType.AND, inner)
    assert str(outer_right) == "bar & (not foo)"


def test_ast_logical_expression_negation():
    expr = AstLogicalExpression(AstAtom("true"), negated=True)
    assert str(expr) == "not true"

    expr_neg_neg = ~expr
    assert str(expr_neg_neg) == "true"
    assert not expr_neg_neg.negated

    # Invert a non-logical expression (wraps it)
    term = AstAtom("true")
    inverted = ~term
    assert isinstance(inverted, AstLogicalExpression)
    assert inverted.negated
    assert str(inverted) == "not true"


def test_ast_logical_expression_no_negation():
    # _as_logical on already logical expression
    expr = AstLogicalExpression(AstAtom("x"))
    # Doing binary op will call _as_logical
    op = AstBinaryOp(expr, BinaryOperatorType.AND, AstAtom("y"))
    assert isinstance(op.left, AstLogicalExpression)
    assert op.left is expr  # Should reuse instance


def test_ast_operators():
    t1 = AstAtom("a")
    t2 = AstAtom("b")

    assert str(t1 & t2) == "a & b"
    assert str(t1 | t2) == "a | b"
    assert str(t1 >= t2) == "a >= b"
    assert str(t1 > t2) == "a > b"
    assert str(t1 <= t2) == "a <= b"
    assert str(t1 < t2) == "a < b"
    assert str(t1 == t2) == "a == b"
    assert str(t1 != t2) == r"a \== b"


def test_coalesce_expr():
    t = AstAtom("a")
    assert str(t & "b") == 'a & "b"'
    assert str(t & 1) == "a & 1"
    assert str(t & 1.5) == "a & 1.5"

    with pytest.raises(TypeError):
        _coalesce_expr(None)


def test_ast_statement():
    stmt = AstStatement(StatementType.DO_ACTION, AstLiteral("action"))
    assert str(stmt) == ".action"


def test_ast_rule():
    # Rule with condition
    rule = AstRule(AstLiteral("head"), AstLiteral("body"))
    assert str(rule) == "head :- body."

    # Rule without condition
    rule_simple = AstRule(AstLiteral("fact"))
    assert str(rule_simple) == "fact."


def test_ast_plan():
    plan = AstPlan(
        TriggerType.ADDED_GOAL,
        AstLiteral("goal"),
        [AstLiteral("context")],
        [AstStatement(StatementType.DO_ACTION, AstLiteral("action"))],
    )
    output = str(plan)
    # verify parts exist
    assert "+!goal" in output
    assert ":   context" in output
    assert "<-  .action." in output


def test_ast_plan_no_context():
    plan = AstPlan(
        TriggerType.ADDED_GOAL,
        AstLiteral("goal"),
        [],
        [AstStatement(StatementType.DO_ACTION, AstLiteral("action"))],
    )
    output = str(plan)
    assert "+!goal" in output
    assert ":   " not in output
    assert "<-  .action." in output


def test_ast_program():
    prog = AstProgram(
        rules=[AstRule(AstLiteral("fact"))],
        plans=[AstPlan(TriggerType.ADDED_BELIEF, AstLiteral("b"), [], [])],
    )
    output = str(prog)
    assert "fact." in output
    assert "+b" in output
