import os
from unittest.mock import MagicMock, patch

from GramAddict.core.llm_provider import extract_json, log_openrouter_burn, query_llm, query_telepathic_llm


def test_extract_json():
    # 1. Normal JSON
    assert extract_json('{"a": 1}') == '{"a": 1}'
    # 2. Markdown JSOn Block
    assert extract_json('```json\n{"a": 1}\n```') == '{"a": 1}'
    # 3. Trailing text
    assert extract_json('{"a": 1}\nSome extra talk') == '{"a": 1}'
    # 4. Incomplete/invalid json (Returns None if no brace match, or garbage if mismatched)
    assert extract_json('{"a": 1') == '{"a": 1}'
    assert extract_json("") is None
    # 5. Nested braces with prefix
    assert extract_json('Here is your response:\n{"a": {"b": 2}}') == '{"a": {"b": 2}}'
    # 6. Think blocks
    assert extract_json('<think>thinking</think>\n{"a":1}') == '{"a":1}'


def test_extract_json_truncation_recovery():
    import json

    # A severely truncated JSON from a local model like qwen3.5:latest
    truncated_text = """{
    "rule_type": "regex",
    "target_attribute": "resource-id",
    "pattern": "com\\.instagram\\.android:id/.*",
    "confidence": 0.95,
    "reasoning": "The target intent req"""

    recovered = extract_json(truncated_text)
    assert recovered is not None

    # It must be parsable now!
    data = json.loads(recovered)
    assert data["rule_type"] == "regex"
    assert data["target_attribute"] == "resource-id"
    assert data["confidence"] == 0.95
    assert data["pattern"] == "com\\.instagram\\.android:id/.*"
    # "reasoning" was cut off midway, so the heuristic should drop it safely instead of failing the parse.
    assert "reasoning" not in data


def test_log_openrouter_burn():
    with (
        patch.dict(os.environ, {"OPENROUTER_API_KEY": "123"}),
        patch("requests.get") as mock_get,
        patch("GramAddict.core.config.Config") as mock_config,
        patch("GramAddict.core.llm_provider.logger.info") as mock_log,
    ):
        mock_config.return_value.args.ai_model_url = "openrouter.ai"
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"data": {"usage": 0.5, "usage_daily": 0.1, "limit": 1.0}}
        mock_get.return_value = mock_resp

        log_openrouter_burn()
        mock_log.assert_called()

    # Exception inside log_openrouter_burn
    with (
        patch.dict(os.environ, {"OPENROUTER_API_KEY": "123"}),
        patch("requests.get") as mock_get,
        patch("GramAddict.core.config.Config") as mock_config,
        patch("GramAddict.core.llm_provider.logger.debug") as mock_debug,
    ):
        mock_config.return_value.args.ai_model_url = "openrouter.ai"
        mock_get.side_effect = Exception("Network Down")

        log_openrouter_burn()
        mock_debug.assert_called()

    # No API Key
    with patch.dict(os.environ, clear=True), patch("requests.get") as mock_get:
        log_openrouter_burn()
        mock_get.assert_not_called()


def test_query_llm_success_openai():
    with patch("requests.post") as mock_post:
        resp = MagicMock()
        resp.status_code = 200
        resp.headers = {"x-openrouter-credits-spent": "0.001"}
        resp.json.return_value = {"choices": [{"message": {"content": '{"test": 1}'}}]}
        mock_post.return_value = resp

        res = query_llm(url="http://api.com/v1/chat/completions", model="gpt-4", prompt="Hello", format_json=True)
        assert res.get("response") == '{"test": 1}'


def test_query_llm_success_ollama():
    with patch("requests.post") as mock_post:
        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {"response": '{"test": 2}'}
        mock_post.return_value = resp

        res = query_llm(
            url="http://api.com",  # no /v1/chat/completions
            model="llama3",
            prompt="Hello",
            format_json=False,
        )
        assert res.get("response") == '{"test": 2}'


def test_query_llm_failed_json_extraction():
    # If formatting demands JSON, but the response is pure text
    with patch("requests.post") as mock_post:
        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {"response": "Not a json"}
        mock_post.return_value = resp

        # Test the branch that raises ValueError inside `query_llm` and defaults to returning None
        res = query_llm(url="http://api.com", model="llama3", prompt="Hello", format_json=True)
        assert res is None


def test_query_llm_http_error_no_fallback():
    with patch("requests.post") as mock_post:
        mock_post.side_effect = Exception("General Network Error")

        res = query_llm(url="http://api.com", model="llama3", prompt="Hello")
        assert res is None


def test_query_telepathic_llm():
    with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
        mock_llm.return_value = {"response": "something"}

        res = query_telepathic_llm("llama3", "http://fake.api", "system", "user")
        assert res == "something"

    # Edge Inference
    with patch("GramAddict.core.config.Config") as MConfig, patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
        mock_llm.return_value = {"response": "edge_response"}
        cfg = MConfig.return_value
        cfg.args.ai_fallback_url = "http://edge.api"
        cfg.args.ai_fallback_model = "edge_model"

        res = query_telepathic_llm("llama3", "http://fake.api", "sys", "usr", use_local_edge=True)
        assert res == "edge_response"

    # Edge fallback config missing
    with patch("GramAddict.core.config.Config", side_effect=Exception("No Config")):
        with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
            mock_llm.return_value = {"response": "fallback"}
            res = query_telepathic_llm("m", "u", "s", "u", use_local_edge=True)
            assert res == "fallback"

    # Nothing returned
    with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
        mock_llm.return_value = None
        assert query_telepathic_llm("m", "u", "s", "u") == "{}"
