"""Prove the refund amount is schema-hidden, resolvers memoize per call, and decline semantics differ per consumer."""

import mcp_types as types

from mcp.client import Client, ClientRequestContext
from stories._harness import Target, run_client


async def main(target: Target, *, mode: str = "auto") -> None:
    # Scripted answers + per-topic counters; topics in `declines` are refused.
    counts = {"scope": 0, "restock": 0}
    answers: dict[str, dict[str, str | int | float | bool | list[str] | None]] = {
        "scope": {"full": True},
        "restock": {"restock": True},
    }
    declines: set[str] = set()

    async def on_elicit(context: ClientRequestContext, params: types.ElicitRequestParams) -> types.ElicitResult:
        assert isinstance(params, types.ElicitRequestFormParams)
        topic = "scope" if "full" in params.requested_schema["properties"] else "restock"
        counts[topic] += 1
        if topic in declines:
            return types.ElicitResult(action="decline")
        return types.ElicitResult(action="accept", content=answers[topic])

    async with Client(target, mode=mode, elicitation_callback=on_elicit) as client:
        # The model-facing contract is order_id + reason only; cents and restock are resolver-filled.
        listed = await client.list_tools()
        (tool,) = listed.tools
        assert set(tool.input_schema["properties"]) == {"order_id", "reason"}, tool.input_schema
        assert set(tool.input_schema.get("required", ())) == {"order_id", "reason"}, tool.input_schema

        # One digital line: scope auto-fills (full), restock auto-fills (False) — zero round-trips.
        receipt = await client.call_tool("refund_order", {"order_id": "ORD-7001", "reason": "download corrupted"})
        assert receipt.structured_content == {
            "order_id": "ORD-7001",
            "refunded_cents": 1500,
            "restocked": False,
            "reason": "download corrupted",
        }, receipt.structured_content
        assert counts == {"scope": 0, "restock": 0}, counts

        # Full refund of a three-line order. The scope question fires exactly ONCE even though
        # both refund_amount and ask_restock consume it — asked at most once per call on either
        # era. ask_restock needs the scope ANSWER, so at 2026 the two questions land in
        # successive rounds, never one concurrent batch: counts and order are era-independent.
        receipt = await client.call_tool("refund_order", {"order_id": "ORD-7002", "reason": "arrived broken"})
        assert receipt.structured_content == {
            "order_id": "ORD-7002",
            "refunded_cents": 4800,
            "restocked": True,
            "reason": "arrived broken",
        }, receipt.structured_content
        assert counts == {"scope": 1, "restock": 1}, counts

        # Declining restock still refunds: the tool keeps the ElicitationResult union for
        # `restock`, sees the decline, and just skips the restock. The scope counter moves
        # again — questions are deduped per call, not per connection.
        declines.add("restock")
        answers["scope"] = {"full": False, "sku": "canvas-tote"}
        receipt = await client.call_tool("refund_order", {"order_id": "ORD-7002", "reason": "wrong colour"})
        assert receipt.structured_content == {
            "order_id": "ORD-7002",
            "refunded_cents": 2400,
            "restocked": False,
            "reason": "wrong colour",
        }, receipt.structured_content
        assert counts == {"scope": 2, "restock": 2}, counts
        declines.clear()

        # An elicited SKU is human-typed: the server validates it against the order before
        # any money is computed.
        answers["scope"] = {"full": False, "sku": "mystery-hat"}
        result = await client.call_tool("refund_order", {"order_id": "ORD-7002", "reason": "lost parcel"})
        assert result.is_error, result
        assert isinstance(result.content[0], types.TextContent)
        assert "order has no item 'mystery-hat'" in result.content[0].text, result.content[0].text

        # Declining scope aborts the whole call: refund_amount and ask_restock both consume scope
        # unwrapped, so whichever resolves first (`cents`, in signature order) aborts, and
        # ask_restock never runs under any order.
        declines.add("scope")
        restock_before = counts["restock"]
        result = await client.call_tool("refund_order", {"order_id": "ORD-7002", "reason": "changed mind"})
        assert result.is_error, result
        assert isinstance(result.content[0], types.TextContent)
        assert "Resolver for parameter 'scope' could not resolve: elicitation was decline" in result.content[0].text, (
            result.content[0].text
        )
        assert counts["restock"] == restock_before, counts
        declines.clear()

        # A ToolError raised inside a resolver surfaces exactly like one from the tool body.
        result = await client.call_tool("refund_order", {"order_id": "ORD-9999", "reason": "typo"})
        assert result.is_error, result
        assert isinstance(result.content[0], types.TextContent)
        assert "unknown order 'ORD-9999'" in result.content[0].text, result.content[0].text

        # Full elicitation trajectory: scope fired in legs 2-5 (memoized within each call),
        # restock only in the two calls that reached it.
        assert counts == {"scope": 4, "restock": 2}, counts


if __name__ == "__main__":
    run_client(main)
