"""Tests for all Tier 1 built-in functions."""
import json
import math
import pytest
import rusty_jq


# ─── length ──────────────────────────────────────────────────────────────────

@pytest.mark.parametrize("query, data, expected", [
    ("length", [1, 2, 3], 3),
    ("length", {"a": 1, "b": 2}, 2),
    ("length", "hello", 5),
    ("length", None, 0),
    (".x | length", {"x": -5}, 5),
    (".x | length", {"x": 3.14}, 3.14),
    (".items | length", {"items": []}, 0),
])
def test_length(query, data, expected):
    assert rusty_jq.compile(query).first(json.dumps(data)) == expected


# ─── keys / keys_unsorted / values ──────────────────────────────────────────

def test_keys_object():
    result = rusty_jq.compile("keys").first(json.dumps({"b": 2, "a": 1, "c": 3}))
    assert result == ["a", "b", "c"]  # sorted

def test_keys_array():
    result = rusty_jq.compile("keys").first(json.dumps([10, 20, 30]))
    assert result == [0, 1, 2]

def test_keys_unsorted():
    prog = rusty_jq.compile("keys_unsorted")
    data = json.dumps({"z": 1, "a": 2, "m": 3})
    result = prog.first(data)
    assert set(result) == {"z", "a", "m"}
    assert len(result) == 3

def test_values_object():
    result = rusty_jq.compile("values").first(json.dumps({"a": 1, "b": 2}))
    assert sorted(result) == [1, 2]

def test_values_array():
    result = rusty_jq.compile("values").first(json.dumps([10, 20]))
    assert result == [10, 20]


# ─── type ────────────────────────────────────────────────────────────────────

@pytest.mark.parametrize("data, expected", [
    (None, "null"),
    (True, "boolean"),
    (42, "number"),
    (3.14, "number"),
    ("hello", "string"),
    ([1, 2], "array"),
    ({"a": 1}, "object"),
])
def test_type(data, expected):
    assert rusty_jq.compile("type").first(json.dumps(data)) == expected


# ─── reverse ─────────────────────────────────────────────────────────────────

def test_reverse_array():
    assert rusty_jq.compile("reverse").first(json.dumps([1, 2, 3])) == [3, 2, 1]

def test_reverse_string():
    assert rusty_jq.compile("reverse").first(json.dumps("abc")) == "cba"


# ─── sort / unique ──────────────────────────────────────────────────────────

def test_sort():
    assert rusty_jq.compile("sort").first(json.dumps([3, 1, 2])) == [1, 2, 3]

def test_sort_strings():
    assert rusty_jq.compile("sort").first(json.dumps(["banana", "apple", "cherry"])) == ["apple", "banana", "cherry"]

def test_unique():
    assert rusty_jq.compile("unique").first(json.dumps([3, 1, 2, 1, 3])) == [1, 2, 3]


# ─── flatten ─────────────────────────────────────────────────────────────────

def test_flatten():
    assert rusty_jq.compile("flatten").first(json.dumps([[1, 2], [3, [4, 5]]])) == [1, 2, 3, 4, 5]

def test_flatten_depth():
    assert rusty_jq.compile("flatten(1)").first(json.dumps([[1, [2]], [3, [4]]])) == [1, [2], 3, [4]]


# ─── add ─────────────────────────────────────────────────────────────────────

def test_add_numbers():
    assert rusty_jq.compile("add").first(json.dumps([1, 2, 3])) == 6

def test_add_strings():
    assert rusty_jq.compile("add").first(json.dumps(["a", "b", "c"])) == "abc"

def test_add_arrays():
    assert rusty_jq.compile("add").first(json.dumps([[1, 2], [3, 4]])) == [1, 2, 3, 4]

def test_add_empty():
    assert rusty_jq.compile("add").first(json.dumps([])) is None


# ─── min / max ───────────────────────────────────────────────────────────────

def test_min():
    assert rusty_jq.compile("min").first(json.dumps([5, 2, 8, 1])) == 1

def test_max():
    assert rusty_jq.compile("max").first(json.dumps([5, 2, 8, 1])) == 8


# ─── first / last ───────────────────────────────────────────────────────────

def test_first():
    assert rusty_jq.compile("first").first(json.dumps([10, 20, 30])) == 10

def test_last():
    assert rusty_jq.compile("last").first(json.dumps([10, 20, 30])) == 30


# ─── not ─────────────────────────────────────────────────────────────────────

@pytest.mark.parametrize("data, expected", [
    (True, False),
    (False, True),
    (None, True),
    (0, False),
    ("", False),
])
def test_not(data, expected):
    assert rusty_jq.compile("not").first(json.dumps(data)) == expected


# ─── empty ───────────────────────────────────────────────────────────────────

def test_empty():
    assert list(rusty_jq.compile("empty").input(json.dumps(42))) == []


# ─── tostring / tonumber ─────────────────────────────────────────────────────

@pytest.mark.parametrize("data, expected", [
    (42, "42"),
    ("hello", "hello"),
    (True, "true"),
    (None, "null"),
])
def test_tostring(data, expected):
    assert rusty_jq.compile("tostring").first(json.dumps(data)) == expected

def test_tonumber_from_string():
    assert rusty_jq.compile("tonumber").first(json.dumps("42")) == 42

def test_tonumber_from_float_string():
    assert rusty_jq.compile("tonumber").first(json.dumps("3.14")) == 3.14

def test_tonumber_passthrough():
    assert rusty_jq.compile("tonumber").first(json.dumps(42)) == 42


# ─── to_entries / from_entries ───────────────────────────────────────────────

def test_to_entries():
    result = rusty_jq.compile("to_entries").first(json.dumps({"a": 1, "b": 2}))
    # result is a list of {"key": k, "value": v} dicts
    keys = sorted([e["key"] for e in result])
    assert keys == ["a", "b"]
    vals = {e["key"]: e["value"] for e in result}
    assert vals == {"a": 1, "b": 2}

def test_from_entries():
    data = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
    result = rusty_jq.compile("from_entries").first(json.dumps(data))
    assert result == {"a": 1, "b": 2}

def test_from_entries_name_key():
    data = [{"name": "x", "value": 10}]
    result = rusty_jq.compile("from_entries").first(json.dumps(data))
    assert result == {"x": 10}


# ─── ascii_downcase / ascii_upcase ──────────────────────────────────────────

def test_ascii_downcase():
    assert rusty_jq.compile("ascii_downcase").first(json.dumps("Hello World")) == "hello world"

def test_ascii_upcase():
    assert rusty_jq.compile("ascii_upcase").first(json.dumps("Hello World")) == "HELLO WORLD"


# ─── tojson / fromjson ──────────────────────────────────────────────────────

def test_tojson():
    result = rusty_jq.compile("tojson").first(json.dumps({"a": 1}))
    parsed = json.loads(result)
    assert parsed == {"a": 1}

def test_fromjson():
    result = rusty_jq.compile("fromjson").first(json.dumps('{"a":1}'))
    assert result == {"a": 1}


# ─── explode / implode ──────────────────────────────────────────────────────

def test_explode():
    result = rusty_jq.compile("explode").first(json.dumps("ABC"))
    assert result == [65, 66, 67]

def test_implode():
    result = rusty_jq.compile("implode").first(json.dumps([65, 66, 67]))
    assert result == "ABC"

def test_explode_implode_roundtrip():
    result = rusty_jq.compile("explode | implode").first(json.dumps("hello"))
    assert result == "hello"


# ─── floor / ceil / round / sqrt / fabs ─────────────────────────────────────

def test_floor():
    assert rusty_jq.compile("floor").first(json.dumps(3.7)) == 3

def test_ceil():
    assert rusty_jq.compile("ceil").first(json.dumps(3.2)) == 4

def test_round():
    assert rusty_jq.compile("round").first(json.dumps(3.5)) == 4

def test_sqrt():
    assert rusty_jq.compile("sqrt").first(json.dumps(9)) == 3.0

def test_fabs():
    assert rusty_jq.compile("fabs").first(json.dumps(-5)) == 5

def test_floor_int_passthrough():
    assert rusty_jq.compile("floor").first(json.dumps(42)) == 42


# ─── has ─────────────────────────────────────────────────────────────────────

def test_has_object():
    assert rusty_jq.compile('has("a")').first(json.dumps({"a": 1})) is True
    assert rusty_jq.compile('has("x")').first(json.dumps({"a": 1})) is False

def test_has_array():
    assert rusty_jq.compile('has(0)').first(json.dumps([10, 20])) is True
    assert rusty_jq.compile('has(5)').first(json.dumps([10, 20])) is False


# ─── startswith / endswith / contains ────────────────────────────────────────

def test_startswith():
    assert rusty_jq.compile('startswith("hel")').first(json.dumps("hello")) is True
    assert rusty_jq.compile('startswith("xyz")').first(json.dumps("hello")) is False

def test_endswith():
    assert rusty_jq.compile('endswith("llo")').first(json.dumps("hello")) is True
    assert rusty_jq.compile('endswith("xyz")').first(json.dumps("hello")) is False

def test_contains_string():
    assert rusty_jq.compile('contains("ell")').first(json.dumps("hello")) is True


# ─── split / join ────────────────────────────────────────────────────────────

def test_split():
    assert rusty_jq.compile('split(",")').first(json.dumps("a,b,c")) == ["a", "b", "c"]

def test_join():
    assert rusty_jq.compile('join("-")').first(json.dumps(["a", "b", "c"])) == "a-b-c"


# ─── ltrimstr / rtrimstr ────────────────────────────────────────────────────

def test_ltrimstr():
    assert rusty_jq.compile('ltrimstr("hel")').first(json.dumps("hello")) == "lo"
    assert rusty_jq.compile('ltrimstr("xyz")').first(json.dumps("hello")) == "hello"

def test_rtrimstr():
    assert rusty_jq.compile('rtrimstr("llo")').first(json.dumps("hello")) == "he"


# ─── index / rindex / indices ────────────────────────────────────────────────

def test_index_string():
    assert rusty_jq.compile('index("bar")').first(json.dumps("foobar")) == 3

def test_index_not_found():
    assert rusty_jq.compile('index("xyz")').first(json.dumps("foobar")) is None

def test_rindex_string():
    assert rusty_jq.compile('rindex("o")').first(json.dumps("foobar")) == 2

def test_indices_string():
    assert rusty_jq.compile('indices("a")').first(json.dumps("banana")) == [1, 3, 5]

def test_index_array():
    assert rusty_jq.compile('index(2)').first(json.dumps([1, 2, 3, 2])) == 1

def test_rindex_array():
    assert rusty_jq.compile('rindex(2)').first(json.dumps([1, 2, 3, 2])) == 3


# ─── recursive descent (..) ─────────────────────────────────────────────────

def test_recursive_descent():
    data = {"a": {"b": 1}, "c": [2, 3]}
    results = list(rusty_jq.compile(".. | select(type == \"number\")").input(json.dumps(data)))
    assert sorted(results) == [1, 2, 3]


# ─── array slicing ──────────────────────────────────────────────────────────

def test_slice():
    assert rusty_jq.compile(".[1:3]").first(json.dumps([0, 1, 2, 3, 4])) == [1, 2]

def test_slice_open_end():
    assert rusty_jq.compile(".[2:]").first(json.dumps([0, 1, 2, 3])) == [2, 3]

def test_slice_open_start():
    assert rusty_jq.compile(".[:2]").first(json.dumps([0, 1, 2, 3])) == [0, 1]

def test_slice_negative():
    assert rusty_jq.compile(".[-2:]").first(json.dumps([0, 1, 2, 3])) == [2, 3]

def test_slice_string():
    assert rusty_jq.compile(".[1:3]").first(json.dumps("hello")) == "el"


# ─── modulo ──────────────────────────────────────────────────────────────────

def test_modulo():
    assert rusty_jq.compile(".a % .b").first(json.dumps({"a": 10, "b": 3})) == 1


# ─── string/array concat with + ─────────────────────────────────────────────

def test_string_concat():
    assert rusty_jq.compile(".a + .b").first(json.dumps({"a": "hello", "b": " world"})) == "hello world"

def test_array_concat():
    assert rusty_jq.compile(".a + .b").first(json.dumps({"a": [1, 2], "b": [3, 4]})) == [1, 2, 3, 4]


# ─── object iterator .[] on objects ──────────────────────────────────────────

def test_object_iterator():
    data = {"a": 1, "b": 2}
    results = list(rusty_jq.compile(".[]").input(json.dumps(data)))
    assert sorted(results) == [1, 2]


# ─── recurse builtin ────────────────────────────────────────────────────────

def test_recurse_numbers():
    """recurse keyword should work same as .."""
    data = {"a": 1, "b": {"c": 2}}
    r1 = sorted(list(rusty_jq.compile('.. | select(type == "number")').input(json.dumps(data))))
    r2 = sorted(list(rusty_jq.compile('recurse | select(type == "number")').input(json.dumps(data))))
    assert r1 == r2


# ─── type selectors via pipeline ────────────────────────────────────────────

def test_numbers_selector():
    """.. | select(type == "number") should filter only numbers from recursive descent."""
    data = {"a": 1, "b": "hello", "c": [2, "x", 3]}
    results = list(rusty_jq.compile('.. | select(type == "number")').input(json.dumps(data)))
    assert sorted(results) == [1, 2, 3]


# ─── pipelines with builtins ────────────────────────────────────────────────

def test_keys_pipe_length():
    assert rusty_jq.compile("keys | length").first(json.dumps({"a": 1, "b": 2, "c": 3})) == 3

def test_split_pipe_length():
    assert rusty_jq.compile('split(",") | length').first(json.dumps("a,b,c")) == 3

def test_to_entries_pipe_map_key():
    result = list(rusty_jq.compile("to_entries | .[] | .key").input(json.dumps({"x": 1, "y": 2})))
    assert sorted(result) == ["x", "y"]
