"""Tests for the s-expression parser."""

import pytest
from shared.sx.parser import parse, parse_all, serialize, ParseError
from shared.sx.types import Symbol, Keyword, NIL


# ---------------------------------------------------------------------------
# Atoms
# ---------------------------------------------------------------------------

class TestAtoms:
    def test_integer(self):
        assert parse("42") == 42

    def test_negative_integer(self):
        assert parse("-7") == -7

    def test_float(self):
        assert parse("3.14") == 3.14

    def test_scientific(self):
        assert parse("1e-3") == 0.001

    def test_string(self):
        assert parse('"hello world"') == "hello world"

    def test_string_escapes(self):
        assert parse(r'"line1\nline2"') == "line1\nline2"
        assert parse(r'"tab\there"') == "tab\there"
        assert parse(r'"say \"hi\""') == 'say "hi"'

    def test_symbol(self):
        assert parse("foo") == Symbol("foo")

    def test_component_symbol(self):
        s = parse("~card")
        assert s == Symbol("~card")
        assert s.is_component

    def test_keyword(self):
        assert parse(":class") == Keyword("class")

    def test_true(self):
        assert parse("true") is True

    def test_false(self):
        assert parse("false") is False

    def test_nil(self):
        assert parse("nil") is NIL


# ---------------------------------------------------------------------------
# Lists
# ---------------------------------------------------------------------------

class TestLists:
    def test_empty_list(self):
        assert parse("()") == []

    def test_simple_list(self):
        assert parse("(1 2 3)") == [1, 2, 3]

    def test_mixed_list(self):
        result = parse('(div :class "main")')
        assert result == [Symbol("div"), Keyword("class"), "main"]

    def test_nested_list(self):
        result = parse("(a (b c) d)")
        assert result == [Symbol("a"), [Symbol("b"), Symbol("c")], Symbol("d")]

    def test_vector_sugar(self):
        assert parse("[1 2 3]") == [1, 2, 3]


# ---------------------------------------------------------------------------
# Maps
# ---------------------------------------------------------------------------

class TestMaps:
    def test_simple_map(self):
        result = parse('{:a 1 :b 2}')
        assert result == {"a": 1, "b": 2}

    def test_nested_map(self):
        result = parse('{:x {:y 3}}')
        assert result == {"x": {"y": 3}}

    def test_string_keys(self):
        result = parse('{"name" "alice"}')
        assert result == {"name": "alice"}


# ---------------------------------------------------------------------------
# Comments
# ---------------------------------------------------------------------------

class TestComments:
    def test_line_comment(self):
        assert parse("; comment\n42") == 42

    def test_inline_comment(self):
        result = parse("(a ; stuff\nb)")
        assert result == [Symbol("a"), Symbol("b")]


# ---------------------------------------------------------------------------
# parse_all
# ---------------------------------------------------------------------------

class TestParseAll:
    def test_multiple(self):
        results = parse_all("1 2 3")
        assert results == [1, 2, 3]

    def test_multiple_lists(self):
        results = parse_all("(a) (b)")
        assert results == [[Symbol("a")], [Symbol("b")]]

    def test_empty(self):
        assert parse_all("") == []
        assert parse_all("  ; only comments\n") == []


# ---------------------------------------------------------------------------
# Quasiquote
# ---------------------------------------------------------------------------

class TestQuasiquote:
    def test_quasiquote_symbol(self):
        result = parse("`x")
        assert result == [Symbol("quasiquote"), Symbol("x")]

    def test_quasiquote_list(self):
        result = parse("`(a b c)")
        assert result == [Symbol("quasiquote"), [Symbol("a"), Symbol("b"), Symbol("c")]]

    def test_unquote(self):
        result = parse(",x")
        assert result == [Symbol("unquote"), Symbol("x")]

    def test_splice_unquote(self):
        result = parse(",@xs")
        assert result == [Symbol("splice-unquote"), Symbol("xs")]

    def test_quasiquote_with_unquote(self):
        result = parse("`(a ,x b)")
        assert result == [Symbol("quasiquote"), [
            Symbol("a"),
            [Symbol("unquote"), Symbol("x")],
            Symbol("b"),
        ]]

    def test_quasiquote_with_splice(self):
        result = parse("`(a ,@rest)")
        assert result == [Symbol("quasiquote"), [
            Symbol("a"),
            [Symbol("splice-unquote"), Symbol("rest")],
        ]]

    def test_roundtrip_quasiquote(self):
        assert serialize(parse("`(a ,x ,@rest)")) == "`(a ,x ,@rest)"

    def test_roundtrip_unquote(self):
        assert serialize(parse(",x")) == ",x"

    def test_roundtrip_splice_unquote(self):
        assert serialize(parse(",@xs")) == ",@xs"


# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------

class TestErrors:
    def test_unterminated_list(self):
        with pytest.raises(ParseError):
            parse("(a b")

    def test_unterminated_string(self):
        with pytest.raises(ParseError):
            parse('"hello')

    def test_unexpected_closer(self):
        with pytest.raises(ParseError):
            parse(")")

    def test_trailing_content(self):
        with pytest.raises(ParseError):
            parse("1 2")


# ---------------------------------------------------------------------------
# Serialization
# ---------------------------------------------------------------------------

class TestSerialize:
    def test_int(self):
        assert serialize(42) == "42"

    def test_float(self):
        assert serialize(3.14) == "3.14"

    def test_string(self):
        assert serialize("hello") == '"hello"'

    def test_string_escapes(self):
        assert serialize('say "hi"') == '"say \\"hi\\""'

    def test_symbol(self):
        assert serialize(Symbol("foo")) == "foo"

    def test_keyword(self):
        assert serialize(Keyword("class")) == ":class"

    def test_bool(self):
        assert serialize(True) == "true"
        assert serialize(False) == "false"

    def test_nil(self):
        assert serialize(None) == "nil"
        assert serialize(NIL) == "nil"

    def test_list(self):
        assert serialize([Symbol("a"), 1, 2]) == "(a 1 2)"

    def test_empty_list(self):
        assert serialize([]) == "()"

    def test_dict(self):
        result = serialize({"a": 1, "b": 2})
        assert result == "{:a 1 :b 2}"

    def test_roundtrip(self):
        original = '(div :class "main" (p "hello") (span 42))'
        assert serialize(parse(original)) == original


# ---------------------------------------------------------------------------
# Reader macros
# ---------------------------------------------------------------------------

class TestReaderMacros:
    """Test #; datum comment, #|...| raw string, and #' quote shorthand."""

    def test_datum_comment_discards(self):
        assert parse_all("#;(ignored) 42") == [42]

    def test_datum_comment_in_list(self):
        assert parse("(1 #;2 3)") == [1, 3]

    def test_datum_comment_nested(self):
        assert parse_all("#;(a (b c) d) 99") == [99]

    def test_raw_string_basic(self):
        assert parse('#|hello|') == "hello"

    def test_raw_string_with_quotes(self):
        assert parse('#|say "hi"|') == 'say "hi"'

    def test_raw_string_with_backslashes(self):
        assert parse('#|a\\nb|') == 'a\\nb'

    def test_raw_string_empty(self):
        assert parse('#||') == ""

    def test_quote_shorthand_symbol(self):
        assert parse("#'foo") == [Symbol("quote"), Symbol("foo")]

    def test_quote_shorthand_list(self):
        assert parse("#'(1 2 3)") == [Symbol("quote"), [1, 2, 3]]

    def test_hash_at_eof_errors(self):
        with pytest.raises(ParseError):
            parse("#")

    def test_unknown_reader_macro_errors(self):
        with pytest.raises(ParseError, match="Unknown reader macro"):
            parse("#x foo")

    def test_extensible_reader_macro(self):
        """Registered reader macros transform the next expression."""
        from shared.sx.parser import register_reader_macro
        register_reader_macro("upper", lambda expr: str(expr).upper())
        try:
            result = parse('#upper "hello"')
            assert result == "HELLO"
        finally:
            from shared.sx.parser import _READER_MACROS
            del _READER_MACROS["upper"]
