"""Bounded Messages-shaped tool loop with scripted assistant fixtures; no API calls."""
from copy import deepcopy
import json

TOOLS = [
    {"name": "get_ticket", "description": "Read the permitted synthetic ticket before proposing a label.",
     "input_schema": {"type": "object", "properties": {"ticket_id": {"type": "string"}}, "required": ["ticket_id"], "additionalProperties": False}},
    {"name": "propose_label", "description": "Propose a label at the supplied record version. Never apply a change.",
     "input_schema": {"type": "object", "properties": {"ticket_id": {"type": "string"}, "label": {"type": "string", "enum": ["billing", "technical", "other"]}, "expected_version": {"type": "integer"}}, "required": ["ticket_id", "label", "expected_version"], "additionalProperties": False}},
]
TICKETS = {"DEMO-42": {"version": 3, "text": "I was charged twice.", "label": "other"}}


class ToolRejected(Exception):
    pass


def execute(name, args, allowed_tickets):
    if name not in {tool["name"] for tool in TOOLS}:
        raise ToolRejected("unknown_tool")
    fields = {"ticket_id"} if name == "get_ticket" else {"ticket_id", "label", "expected_version"}
    if not isinstance(args, dict) or set(args) != fields:
        raise ToolRejected("invalid_fields")
    ticket_id = args["ticket_id"]
    if not isinstance(ticket_id, str) or ticket_id not in allowed_tickets:
        raise ToolRejected("record_unavailable")
    ticket = TICKETS.get(ticket_id)
    if ticket is None:
        raise ToolRejected("record_unavailable")
    if name == "get_ticket":
        return {"ticket_id": ticket_id, **ticket}
    if not isinstance(args["label"], str) or args["label"] not in {"billing", "technical", "other"}:
        raise ToolRejected("invalid_label")
    if type(args["expected_version"]) is not int or args["expected_version"] != ticket["version"]:
        raise ToolRejected("stale_or_invalid_version")
    return {**args, "applied": False}


def run_loop(model, *, allowed_tickets=frozenset({"DEMO-42"}), max_turns=3, max_calls=2):
    messages = [{"role": "user", "content": "Inspect DEMO-42 and propose a label. Do not change it."}]
    trace, seen_ids = [], set()
    calls = 0
    for turn in range(max_turns):
        response = model(deepcopy(messages), deepcopy(TOOLS))
        if not isinstance(response, dict) or not isinstance(response.get("content"), list):
            return {"state": "invalid_response", "trace": trace}
        blocks = response["content"]
        if any(not isinstance(block, dict) for block in blocks):
            return {"state": "invalid_response", "trace": trace}
        uses = [block for block in blocks if block.get("type") == "tool_use"]
        if response.get("stop_reason") == "end_turn":
            texts = [b["text"] for b in blocks if b.get("type") == "text" and isinstance(b.get("text"), str)]
            if uses or not texts or any(b.get("type") != "text" for b in blocks):
                return {"state": "invalid_response", "trace": trace}
            return {"state": "finished", "text": "\n".join(texts), "trace": trace}
        if response.get("stop_reason") != "tool_use":
            return {"state": "incomplete_response", "trace": trace}
        if len(uses) != 1 or any(b.get("type") not in {"text", "tool_use"} for b in blocks):
            return {"state": "unsupported_tool_batch", "trace": trace}
        call = uses[0]
        call_id = call.get("id")
        if not isinstance(call_id, str) or not call_id or not isinstance(call.get("name"), str):
            return {"state": "invalid_response", "trace": trace}
        if call_id in seen_ids:
            return {"state": "duplicate_call_id", "trace": trace}
        if calls >= max_calls:
            return {"state": "tool_budget_exhausted", "trace": trace}
        seen_ids.add(call_id)
        calls += 1
        try:
            result, is_error = execute(call["name"], call.get("input"), allowed_tickets), False
        except ToolRejected as error:
            result, is_error = {"error": str(error)}, True
        trace.append({"turn": turn+1, "call_id": call_id, "tool": call["name"], "is_error": is_error, "result": result})
        messages.extend([
            {"role": "assistant", "content": blocks},
            {"role": "user", "content": [{"type": "tool_result", "tool_use_id": call_id,
                                              "content": json.dumps(result), "is_error": is_error}]},
        ])
    return {"state": "turn_budget_exhausted", "trace": trace}


def tool(call_id, name, args):
    return {"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": call_id, "name": name, "input": args}]}


def done(text="Proposal ready for review; nothing applied."):
    return {"stop_reason": "end_turn", "content": [{"type": "text", "text": text}]}


class ScriptedAssistant:
    def __init__(self, replies):
        self.replies, self.index = replies, 0
    def __call__(self, messages, tools):
        assert len(tools) == 2
        if self.index:
            previous = self.replies[self.index-1]
            call = [b for b in previous["content"] if b["type"] == "tool_use"][0]
            assert messages[-2] == {"role": "assistant", "content": previous["content"]}
            assert messages[-1]["role"] == "user"
            assert messages[-1]["content"][0]["tool_use_id"] == call["id"]
        reply = self.replies[self.index]
        self.index += 1
        return deepcopy(reply)


def fixtures():
    original = deepcopy(TICKETS)
    read = tool("call-1", "get_ticket", {"ticket_id": "DEMO-42"})
    propose = tool("call-2", "propose_label", {"ticket_id": "DEMO-42", "label": "billing", "expected_version": 3})
    cases = [
        ("read-propose-finish", [read, propose, done()], "finished", None),
        ("record-not-permitted", [tool("x", "get_ticket", {"ticket_id": "PRIVATE-7"}), done("Record unavailable.")], "finished", "record_unavailable"),
        ("unknown-tool", [tool("x", "send_email", {}), done("Unsupported operation.")], "finished", "unknown_tool"),
        ("extra-argument", [tool("x", "get_ticket", {"ticket_id": "DEMO-42", "authorized": True}), done("Invalid input.")], "finished", "invalid_fields"),
        ("stale-version", [tool("x", "propose_label", {"ticket_id": "DEMO-42", "label": "billing", "expected_version": 2}), done("Reload the record.")], "finished", "stale_or_invalid_version"),
        ("tool-budget", [tool(str(i), "get_ticket", {"ticket_id": "DEMO-42"}) for i in range(3)], "tool_budget_exhausted", None),
        ("duplicate-call", [read, read], "duplicate_call_id", None),
        ("output-limit", [{"stop_reason": "max_tokens", "content": []}], "incomplete_response", None),
        ("parallel-batch", [{"stop_reason": "tool_use", "content": read["content"]+propose["content"]}], "unsupported_tool_batch", None),
        ("malformed-response", [{"content": "not blocks"}], "invalid_response", None),
    ]
    results = []
    for name, replies, state, error in cases:
        result = run_loop(ScriptedAssistant(replies))
        assert result["state"] == state, (name, result)
        if error:
            assert result["trace"][0]["is_error"] and result["trace"][0]["result"] == {"error": error}
        if name == "read-propose-finish":
            assert result["trace"][-1]["result"] == {"ticket_id": "DEMO-42", "label": "billing", "expected_version": 3, "applied": False}
        results.append({"case": name, **result})
    assert TICKETS == original
    print(json.dumps({"mode": "scripted assistant; real Python tool handlers; no model call", "passed": len(results), "ticket_unchanged": True, "results": results}, indent=2))


if __name__ == "__main__":
    if not __debug__:
        raise SystemExit("Run fixture assertions without -O")
    fixtures()
