"""Contract-first drift guard.

Loads the vendored customer OpenAPI spec and asserts that every operationId is either
mapped to a real, callable SDK method (OP_MAP) or explicitly listed as intentionally
unwrapped (UNWRAPPED). A new operation added to the contract fails this test until it is
either wrapped or consciously deferred — so the SDK can never silently drift from the API.

Refresh the fixture from the platform on contract changes:
    cp <platform>/packages/contracts/openapi/absolutepay-customer.json tests/openapi/
"""

import json
from pathlib import Path

import pytest

from absolutepay import AbsolutePay

SPEC = Path(__file__).parent / "openapi" / "absolutepay-customer.json"

# operationId -> dotted attribute path of the wrapping method on an AbsolutePay instance.
OP_MAP = {
    "getBalances": "balances.list",
    "previewFee": "fees.preview",
    "createRefund": "refunds.create",
    "getRefund": "refunds.get",
    "listRefunds": "refunds.list",
    "createPayout": "payouts.create",
    "listWithdrawOptions": "payouts.options",
    "getPayout": "payouts.get",
    "previewConversion": "conversions.quote",
    "executeConversion": "conversions.execute",
    "listConversions": "conversions.list",
    "createSubscriptionPlan": "subscriptions.plans.create",
    "listSubscriptionPlans": "subscriptions.plans.list",
    "createSubscription": "subscriptions.create",
    "listSubscriptions": "subscriptions.list",
    "listDeductions": "subscriptions.deductions",
    "cancelSubscription": "subscriptions.cancel",
    "createCheckoutLink": "checkouts.create",
    "listCheckouts": "checkouts.list",
    "getCheckout": "checkouts.get",
    "updateCheckout": "checkouts.update",
    "deleteCheckout": "checkouts.delete",
    "createInvoice": "invoices.create",
    "listInvoices": "invoices.list",
    "getInvoice": "invoices.get",
    "updateInvoice": "invoices.update",
    "deleteInvoice": "invoices.delete",
    "listOffRampCountries": "offramp.countries",
    "listOffRampBanks": "offramp.banks",
    "offRampQuote": "offramp.quote",
    "offRampWithdraw": "offramp.withdraw",
    "listOffRampOrders": "offramp.orders",
    "listGiftTemplates": "giftcards.templates",
    "createGiftCard": "giftcards.create",
    "listGiftCards": "giftcards.list",
    "getGiftCard": "giftcards.get",
    "listReconciliationPayments": "reconciliation.payments",
    "listReconciliationWithdrawals": "reconciliation.withdrawals",
    "listDepositChains": "deposits.chains",
    "createDepositAddress": "deposits.create_address",
    "listDeposits": "deposits.list",
    "listDepositAddresses": "deposits.addresses",
    "getDepositAddress": "deposits.get_address",
    "registerOffRampBank": "offramp.register_bank",
    "deleteOffRampBank": "offramp.remove_bank",
    "submitOffRampBankMaterials": "offramp.submit_bank_materials",
}

# Operations deliberately NOT wrapped (documented gaps, not drift). The SDK now covers the
# entire customer contract, so this set is empty.
UNWRAPPED: set[str] = set()


def _spec_operation_ids() -> set[str]:
    spec = json.loads(SPEC.read_text())
    ids = set()
    for path_item in spec["paths"].values():
        for method, op in path_item.items():
            if method in ("get", "post", "put", "patch", "delete") and "operationId" in op:
                ids.add(op["operationId"])
    return ids


def _resolve(client, dotted: str):
    obj = client
    for part in dotted.split("."):
        obj = getattr(obj, part)
    return obj


def test_every_operation_is_mapped_or_deferred():
    ids = _spec_operation_ids()
    known = set(OP_MAP) | UNWRAPPED
    missing = ids - known
    assert not missing, f"contract has operations neither wrapped nor deferred: {sorted(missing)}"


def test_no_operation_double_listed():
    overlap = set(OP_MAP) & UNWRAPPED
    assert not overlap, f"operations listed as both wrapped and deferred: {sorted(overlap)}"


def test_map_targets_do_not_reference_stale_operations():
    # Every OP_MAP / UNWRAPPED key must still exist in the contract.
    ids = _spec_operation_ids()
    stale = (set(OP_MAP) | UNWRAPPED) - ids
    assert not stale, f"OP_MAP/UNWRAPPED reference operations no longer in the contract: {sorted(stale)}"


@pytest.mark.parametrize("op_id,dotted", sorted(OP_MAP.items()))
def test_mapped_method_exists_and_is_callable(op_id, dotted):
    client = AbsolutePay("ap_test_x", base_url="http://localhost")
    method = _resolve(client, dotted)
    assert callable(method), f"{op_id} -> {dotted} is not callable"
