"""A 400 for the model's own malformed tool-call JSON must not walk the fallback chain (#12770)."""
from types import SimpleNamespace
from unittest.mock import patch

import pytest

from agent.error_classifier import FailoverReason, classify_api_error
from agent.turn_api_error import settle_unrecovered_error


class _Err(Exception):
    status_code = 400
    response = None

    def __init__(self, message, body=None):
        super().__init__(message)
        self.body = body or {"error": {"message": message, "type": "invalid_request_error"}}


@pytest.mark.parametrize("wording", ["invalid tool call arguments", "Invalid function_call arguments"])
def test_malformed_tool_args_400_is_terminal_without_fallback(wording):
    """Large session included: the bare-ish message must not read as context overflow either."""
    verdict = classify_api_error(_Err(f"Error code: 400 - {wording}"), provider="ollama",
                                 approx_tokens=90_000, context_length=128_000, num_messages=120)
    assert verdict.reason is FailoverReason.format_error
    assert (verdict.retryable, verdict.should_compress, verdict.should_fallback) == (False, False, False)

    # Unrelated request-shape 400s keep their fallback (another provider may accept the request).
    assert classify_api_error(_Err("Unsupported parameter: 'max_tokens'")).should_fallback is True


class _Agent:
    """Only the fallback seam is real; every other helper the terminal path touches is a no-op."""
    log_prefix = ""
    verbose = False
    provider = "ollama"
    _fallback_chain = [object()]
    _fallback_index = 0
    _credential_pool = None

    def __init__(self):
        self.activated = []

    def _has_pending_fallback(self):
        return True

    def _try_activate_fallback(self, **kwargs):
        self.activated.append(True)
        return True

    def _summarize_api_error(self, error):
        return str(error)

    def __getattr__(self, name):
        return lambda *args, **kwargs: None


def _settle(agent, err, provider, status_code):
    retry = SimpleNamespace(copilot_stale_cred_retry_attempted=False, primary_recovery_attempted=False)
    classified = classify_api_error(err, provider=provider)
    with patch("agent.conversation_loop._is_copilot_provider", lambda a: False):
        return settle_unrecovered_error(
            agent, api_error=err, classified=classified, _retry=retry, status_code=status_code, error_msg=str(err),
            is_context_length_error=False, is_rate_limited=False, _is_zai_coding_overload=False,
            _provider=provider, _base="http://127.0.0.1:11434/v1", _model="glm", messages=[], api_messages=[],
            api_kwargs={}, active_system_prompt="", conversation_history=None, approx_tokens=10,
            retry_count=0, max_retries=3, compression_attempts=0, api_call_count=1,
        )


def test_client_error_settlement_skips_fallback_when_classifier_says_so():
    agent = _Agent()
    verdict = _settle(agent, _Err("invalid tool call arguments"), "ollama", 400)
    assert verdict.action == "return"
    assert verdict.result["failure_reason"] == FailoverReason.format_error.value
    assert agent.activated == []


def test_fallback_free_verdict_wins_over_local_valueerror_shape():
    """A recognised no-fallback verdict raised as a ValueError subclass (MoA preset missing,
    #55933) must not sneak through the unclassified-local-error fallback allowance; a truly
    unclassified ValueError keeps it."""
    from agent.errors import MoAPresetNotFoundError

    agent = _Agent()
    verdict = _settle(agent, MoAPresetNotFoundError("MoA preset 'old' was not found"), "moa", None)
    assert verdict.action == "return"
    assert agent.activated == []

    agent = _Agent()
    verdict = _settle(agent, ValueError("some local bug"), "openai", None)
    assert verdict.action == "break"
    assert agent.activated == [True]
