"""
Tests of calf.parser
"""

import calf.parser as cp
from conftest import parametrize
import pytest


@parametrize(
    "text",
    [
        '"',
        '"foo bar',
        '"""foo bar',
        '"""foo bar"',
    ],
)
def test_bad_strings_raise(text):
    """Tests asserting we won't let obviously bad strings fly."""
    # FIXME (arrdem 2021-03-13):
    #   Can we provide this behavior in the lexer rather than in the parser?
    with pytest.raises(ValueError):
        next(cp.parse_buffer(text))


@parametrize(
    "text",
    [
        "[1.0",
        "(1.0",
        "{1.0",
    ],
)
def test_unterminated_raises(text):
    """Tests asserting that we don't let unterminated collections parse."""
    with pytest.raises(cp.CalfMissingCloseParseError):
        next(cp.parse_buffer(text))


@parametrize(
    "text",
    [
        "[{]",
        "[(]",
        "({)",
        "([)",
        "{(}",
        "{[}",
    ],
)
def test_unbalanced_raises(text):
    """Tests asserting that we don't let missmatched collections parse."""
    with pytest.raises(cp.CalfUnexpectedCloseParseError):
        next(cp.parse_buffer(text))


@parametrize(
    "buff, value",
    [
        ('"foo"', "foo"),
        ('"foo\tbar"', "foo\tbar"),
        ('"foo\n\rbar"', "foo\n\rbar"),
        ('"foo\\"bar\\""', 'foo"bar"'),
        ('"""foo"""', "foo"),
        ('"""foo"bar"baz"""', 'foo"bar"baz'),
    ],
)
def test_strings_round_trip(buff, value):
    assert next(cp.parse_buffer(buff)) == value


@parametrize(
    "text, element_types",
    [
        # Integers
        ("(1)", ["INTEGER"]),
        ("( 1 )", ["INTEGER"]),
        ("(,1,)", ["INTEGER"]),
        ("(1\n)", ["INTEGER"]),
        ("(\n1\n)", ["INTEGER"]),
        ("(1, 2, 3, 4)", ["INTEGER", "INTEGER", "INTEGER", "INTEGER"]),
        # Floats
        ("(1.0)", ["FLOAT"]),
        ("(1.0e0)", ["FLOAT"]),
        ("(1e0)", ["FLOAT"]),
        ("(1e0)", ["FLOAT"]),
        # Symbols
        ("(foo)", ["SYMBOL"]),
        ("(+)", ["SYMBOL"]),
        ("(-)", ["SYMBOL"]),
        ("(*)", ["SYMBOL"]),
        ("(foo-bar)", ["SYMBOL"]),
        ("(+foo-bar+)", ["SYMBOL"]),
        ("(+foo-bar+)", ["SYMBOL"]),
        ("( foo bar )", ["SYMBOL", "SYMBOL"]),
        # Keywords
        ("(:foo)", ["KEYWORD"]),
        ("( :foo )", ["KEYWORD"]),
        ("(\n:foo\n)", ["KEYWORD"]),
        ("(,:foo,)", ["KEYWORD"]),
        ("(:foo :bar)", ["KEYWORD", "KEYWORD"]),
        ("(:foo :bar 1)", ["KEYWORD", "KEYWORD", "INTEGER"]),
        # Strings
        ('("foo", "bar", "baz")', ["STRING", "STRING", "STRING"]),
        # Lists
        ("([] [] ())", ["SQLIST", "SQLIST", "LIST"]),
    ],
)
def test_parse_list(text, element_types):
    """Test we can parse various lists of contents."""
    l_t = next(cp.parse_buffer(text, discard_whitespace=True))
    assert l_t.type == "LIST"
    assert [t.type for t in l_t] == element_types


@parametrize(
    "text, element_types",
    [
        # Integers
        ("[1]", ["INTEGER"]),
        ("[ 1 ]", ["INTEGER"]),
        ("[,1,]", ["INTEGER"]),
        ("[1\n]", ["INTEGER"]),
        ("[\n1\n]", ["INTEGER"]),
        ("[1, 2, 3, 4]", ["INTEGER", "INTEGER", "INTEGER", "INTEGER"]),
        # Floats
        ("[1.0]", ["FLOAT"]),
        ("[1.0e0]", ["FLOAT"]),
        ("[1e0]", ["FLOAT"]),
        ("[1e0]", ["FLOAT"]),
        # Symbols
        ("[foo]", ["SYMBOL"]),
        ("[+]", ["SYMBOL"]),
        ("[-]", ["SYMBOL"]),
        ("[*]", ["SYMBOL"]),
        ("[foo-bar]", ["SYMBOL"]),
        ("[+foo-bar+]", ["SYMBOL"]),
        ("[+foo-bar+]", ["SYMBOL"]),
        ("[ foo bar ]", ["SYMBOL", "SYMBOL"]),
        # Keywords
        ("[:foo]", ["KEYWORD"]),
        ("[ :foo ]", ["KEYWORD"]),
        ("[\n:foo\n]", ["KEYWORD"]),
        ("[,:foo,]", ["KEYWORD"]),
        ("[:foo :bar]", ["KEYWORD", "KEYWORD"]),
        ("[:foo :bar 1]", ["KEYWORD", "KEYWORD", "INTEGER"]),
        # Strings
        ('["foo", "bar", "baz"]', ["STRING", "STRING", "STRING"]),
        # Lists
        ("[[] [] ()]", ["SQLIST", "SQLIST", "LIST"]),
    ],
)
def test_parse_sqlist(text, element_types):
    """Test we can parse various 'square' lists of contents."""
    l_t = next(cp.parse_buffer(text, discard_whitespace=True))
    assert l_t.type == "SQLIST"
    assert [t.type for t in l_t] == element_types


@parametrize(
    "text, element_pairs",
    [
        ("{}", []),
        ("{:foo 1}", [["KEYWORD", "INTEGER"]]),
        ("{:foo 1, :bar 2}", [["KEYWORD", "INTEGER"], ["KEYWORD", "INTEGER"]]),
        ("{foo 1, bar 2}", [["SYMBOL", "INTEGER"], ["SYMBOL", "INTEGER"]]),
        ("{foo 1, bar -2}", [["SYMBOL", "INTEGER"], ["SYMBOL", "INTEGER"]]),
        ("{foo 1, bar -2e0}", [["SYMBOL", "INTEGER"], ["SYMBOL", "FLOAT"]]),
        ("{foo ()}", [["SYMBOL", "LIST"]]),
        ("{foo []}", [["SYMBOL", "SQLIST"]]),
        ("{foo {}}", [["SYMBOL", "DICT"]]),
        ('{"foo" {}}', [["STRING", "DICT"]]),
    ],
)
def test_parse_dict(text, element_pairs):
    """Test we can parse various mappings."""
    d_t = next(cp.parse_buffer(text, discard_whitespace=True))
    assert d_t.type == "DICT"
    assert [[t.type for t in pair] for pair in d_t.value] == element_pairs


@parametrize("text", ["{1}", "{1, 2, 3}", "{:foo}", "{:foo :bar :baz}"])
def test_parse_bad_dict(text):
    """Assert that dicts with missmatched pairs don't parse."""
    with pytest.raises(Exception):
        next(cp.parse_buffer(text))


@parametrize(
    "text",
    [
        "()",
        "(1 1.1 1e2 -2 foo :foo foo/bar :foo/bar [{},])",
        "{:foo bar, :baz [:qux]}",
        "'foo",
        "'[foo bar :baz 'qux, {}]",
        "#foo []",
        "^{} bar",
    ],
)
def test_examples(text):
    """Shotgun examples showing we can parse some stuff."""

    assert list(cp.parse_buffer(text))