"""Tests for crystallise.common.json_utils."""
import pytest

from crystallise.common.json_utils import parse_llm_json, parse_json_safe, LLMParseError


class TestParseLlmJson:
    """Tests for the new unified LLM JSON parser."""

    def test_clean_json_object(self):
        result = parse_llm_json('{"key": "value"}')
        assert result == {"key": "value"}

    def test_clean_json_array(self):
        result = parse_llm_json('[1, 2, 3]')
        assert result == [1, 2, 3]

    def test_markdown_fence_json(self):
        text = '```json\n{"key": "val"}\n```'
        result = parse_llm_json(text)
        assert result == {"key": "val"}

    def test_markdown_fence_no_language(self):
        text = '```\n{"key": "val"}\n```'
        result = parse_llm_json(text)
        assert result == {"key": "val"}

    def test_markdown_fence_python_language(self):
        text = '```python\n[1, 2, 3]\n```'
        result = parse_llm_json(text)
        assert result == [1, 2, 3]

    def test_json_with_surrounding_text(self):
        text = 'Here are the results:\n{"a": 1}\nHope this helps!'
        result = parse_llm_json(text)
        assert result == {"a": 1}

    def test_array_with_surrounding_text(self):
        text = 'The suggestions are:\n[{"name": "x"}]\nDone.'
        result = parse_llm_json(text)
        assert result == [{"name": "x"}]

    def test_expect_object_with_object(self):
        result = parse_llm_json('{"a": 1}', expect="object")
        assert result == {"a": 1}

    def test_expect_object_with_array_raises(self):
        with pytest.raises(LLMParseError, match="Expected JSON object"):
            parse_llm_json('[1, 2]', expect="object")

    def test_expect_array_with_array(self):
        result = parse_llm_json('[1, 2]', expect="array")
        assert result == [1, 2]

    def test_expect_array_with_object_raises(self):
        with pytest.raises(LLMParseError, match="Expected JSON array"):
            parse_llm_json('{"a": 1}', expect="array")

    def test_expect_any_accepts_both(self):
        assert parse_llm_json('{"a": 1}', expect="any") == {"a": 1}
        assert parse_llm_json('[1]', expect="any") == [1]

    def test_empty_string_raises(self):
        with pytest.raises(LLMParseError, match="Empty response"):
            parse_llm_json("")

    def test_none_raises(self):
        with pytest.raises(LLMParseError, match="Empty response"):
            parse_llm_json(None)

    def test_no_json_raises(self):
        with pytest.raises(LLMParseError, match="No valid JSON"):
            parse_llm_json("This has no JSON at all")

    def test_truncated_json_raises(self):
        with pytest.raises(LLMParseError, match="No valid JSON"):
            parse_llm_json('{"key": "value", "incomplete":')

    def test_error_has_raw_text(self):
        try:
            parse_llm_json("not json")
            assert False, "Should have raised"
        except LLMParseError as e:
            assert e.raw_text == "not json"
            assert e.reason == "No valid JSON found in response"

    def test_wrapper_object_with_suggestions_key(self):
        """Common LLM pattern: wrapping array in an object."""
        text = '{"suggestions": [{"name": "a"}, {"name": "b"}]}'
        result = parse_llm_json(text, expect="object")
        assert "suggestions" in result

    def test_nested_markdown_fence(self):
        text = '```json\n{"outer": {"inner": [1, 2, 3]}}\n```'
        result = parse_llm_json(text)
        assert result == {"outer": {"inner": [1, 2, 3]}}

    def test_json_with_newlines_inside(self):
        text = '{"key": "line1\\nline2", "num": 1}'
        result = parse_llm_json(text)
        assert result["key"] == "line1\nline2"

    def test_deeply_nested_json(self):
        text = '{"a": {"b": {"c": {"d": 1}}}}'
        result = parse_llm_json(text)
        assert result["a"]["b"]["c"]["d"] == 1

    def test_json_with_unicode(self):
        text = '{"name": "André", "city": "São Paulo"}'
        result = parse_llm_json(text)
        assert result["name"] == "André"
        assert result["city"] == "São Paulo"

    def test_json_with_special_chars_in_values(self):
        text = '{"text": "He said \\"hello\\" to me"}'
        result = parse_llm_json(text)
        assert "hello" in result["text"]

    def test_multiple_json_objects_returns_first(self):
        text = '{"first": 1} some text {"second": 2}'
        result = parse_llm_json(text)
        assert result == {"first": 1}

    def test_whitespace_only_raises(self):
        with pytest.raises(LLMParseError, match="Empty response"):
            parse_llm_json("   \n\t  ")


class TestParseJsonSafe:
    def test_valid_json(self):
        assert parse_json_safe('{"a": 1}') == {"a": 1}

    def test_valid_array(self):
        assert parse_json_safe('[1, 2]') == [1, 2]

    def test_invalid_returns_none(self):
        assert parse_json_safe("not json") is None

    def test_empty_returns_none(self):
        assert parse_json_safe("") is None

    def test_none_returns_none(self):
        assert parse_json_safe(None) is None
