from logitbias_parser import parse
from logitbias import (
    OpAdd,
    OpMultiply,
    OpDivide,
    OpMod,
    OpAnd,
    OpOr,
    OpCompare,
    OpIn,
    OpVariableAccess,
    OpArrayAccess,
    escape_to_zero,
    eval,
    logitbias_simplify,
)
from logitbias_json import json2logitbias, logitbias_eval_all
import pytest

parser_tests = [
    ("1", 1),
    ("1 + 2", OpAdd(1, 2)),
    ("!(1 + 2)", OpCompare(False, OpAdd(1, 2), "==")),
    ("1 - 2", OpAdd(1, 2, rhs_flip_sign=True)),
    ("1 + 2 * 3", OpAdd(1, OpMultiply(2, 3))),
    ("length > 0.25", OpCompare(OpVariableAccess("length"), 0.25, ">")),
    (
        "pitch <= pitches[-1]",
        OpCompare(OpVariableAccess("pitch"), OpArrayAccess("pitches", -1), "<="),
    ),
    ("index === 1", OpCompare(OpVariableAccess("index"), 1, "==")),
    ("index == 1", OpCompare(OpVariableAccess("index"), 1, "==")),
    ("index != 1", OpCompare(OpVariableAccess("index"), 1, "!=")),
    (
        "1.5 <= beat < 1.75",
        OpAnd(
            OpCompare(1.5, OpVariableAccess("beat"), "<="),
            OpCompare(OpVariableAccess("beat"), 1.75, "<"),
        ),
    ),
    ("pitch % 12 in [1,2,3]", OpIn(OpMod(OpVariableAccess("pitch"), 12), [1, 2, 3])),
    ("0.33 in [1/3, 0.3]", OpIn(0.33, [OpDivide(1, 3), 0.3])),
    ("true", True),
    ("false", False),
]


@pytest.mark.parametrize("input,expected", parser_tests)
def test_parser(input, expected):
    assert parse(input) == expected


def raiser():
    raise Exception("whoops")


def test_eval_raises():
    with pytest.raises(Exception):
        eval(parse("pitch > 0"), {"pitch": raiser})


eval_tests = [
    ("1+2+3-4*5/2", {}, 1 + 2 + 3 - 4 * 5 / 2),
    ("!(1 == 2)", {}, True),
    ("pitch % 12 in [1,2,3,5,6,8,10,11]", {"pitch": 61}, True),
    ("pitch % 12 in [1,2,3,5,6,8,10,11]", {"pitch": 57}, False),
    ("index === 2", {"index": 2}, True),
    ("pitch <= pitches[-1]", {"pitch": 61, "pitches": [60, 61, 62]}, True),
    ("pitch > pitches[-1]", {"pitch": 61, "pitches": [60, 61, 62]}, False),
    ("pitch > 0", {"pitch": lambda: 1}, True),
    ("1 < 2 || pitch > 0", {"pitch": raiser}, True),
    ("0.333333 in [1/3]", {}, True),
    ("0.3 in [1/3, 0.4, 5+6]", {}, False),
]


@pytest.mark.parametrize("input,env,expected", eval_tests)
def test_eval(input, env, expected):
    assert eval(parse(input), env) == expected


json_tests = [
    ({"true": 1}, [(True, 1)]),
    (
        {
            "true": {
                "length > 0.25": -100,
                "length > 0.50": -1000,
            },
            "pitch >= pitches[-1]": {
                "index === 1": -101,
            },
            "pitch <= pitches[-1]": {
                "index === 2": -102,
            },
        },
        [
            (OpAnd(True, OpCompare(OpVariableAccess("length"), 0.25, ">")), -100),
            (OpAnd(True, OpCompare(OpVariableAccess("length"), 0.5, ">")), -1000),
            (
                OpAnd(
                    OpCompare(
                        OpVariableAccess("pitch"), OpArrayAccess("pitches", -1), ">="
                    ),
                    OpCompare(OpVariableAccess("index"), 1, "=="),
                ),
                -101,
            ),
            (
                OpAnd(
                    OpCompare(
                        OpVariableAccess("pitch"), OpArrayAccess("pitches", -1), "<="
                    ),
                    OpCompare(OpVariableAccess("index"), 2, "=="),
                ),
                -102,
            ),
        ],
    ),
]


@pytest.mark.parametrize("input,expected", json_tests)
def test_json(input, expected):
    assert json2logitbias(input) == expected


eval_all_inp1 = {
    "true": {
        "length > 0.25": -100,
        "length > 0.50": -1000,
    },
    "pitch >= pitches[-1]": {
        "index === 1": -101,
    },
}
eval_all_tests = [
    (
        eval_all_inp1,
        {"length": 0.3, "pitch": 61, "pitches": [60, 61, 62], "index": 1},
        -100,
    ),
    (
        eval_all_inp1,
        {"length": 0.6, "pitch": 63, "pitches": [60, 61, 62], "index": 1},
        -1201,
    ),
]


@pytest.mark.parametrize("input,env,expected", eval_all_tests)
def test_eval_all(input, env, expected):
    assert logitbias_eval_all(json2logitbias(input), env) == expected


simplify_inp1 = {
    "true": {
        "length > 0.25": -100,
        "length > 0.50": -1000,
    },
    "pitch >= pitches[-1]": {
        "index === 1": -101,
    },
    "pitch == 0 && nonvar == 1": -10000,
    "pitch == 50 && escape": -1,
}
simplify_tests = [
    (
        simplify_inp1,
        {"length": 0.3, "pitch": 61, "pitches": [60, 61, 62], "index": 1, "nonvar": 0},
        "foo",
        [None],
        [-100],
    ),
    (
        simplify_inp1,
        {"pitch": 70, "pitches": [60, 61, 62], "index": 1},
        "length",
        [0.3, 0.7],
        [-201, -1201],
    ),
    (
        simplify_inp1,
        {
            "length": 0,
            "index": 0,
            "pitch": 50,
            "pitches": [60, 61, 62],
            "escape": escape_to_zero,
        },
        "nonvar",
        [None],
        [0],
    ),
]


@pytest.mark.parametrize("input,env,variable,vals,expecteds", simplify_tests)
def test_simplify(input, env, variable, vals, expecteds):
    f = logitbias_simplify(json2logitbias(input), env, variable)
    for val, expected in zip(vals, expecteds):
        if val is None:
            assert f == expected
        else:
            assert f(val) == expected
