import './assert_dev_env.js';
/**
 * JSON-RPC test helpers — request construction, response assertion, and
 * one-shot call ergonomics.
 *
 * Shared by `testing/rpc_attack_surface.ts`, `testing/rpc_round_trip.ts`, and
 * consumer-facing admin/audit suites that exercise RPC methods directly.
 *
 * @module
 */
import { assert } from 'vitest';
import { z } from 'zod';
import { JSONRPC_VERSION, JsonrpcErrorResponse, JsonrpcResponse, } from '../http/jsonrpc.js';
import { create_stub_app_server_context } from './stubs.js';
/**
 * Resolve a suite's `rpc_endpoints` option to an array for setup-time
 * inspection (path lookup, action presence checks).
 *
 * For the factory form this invokes the factory twice with stub
 * `AppServerContext`s and asserts that both invocations produce the same
 * (path, method-list) shape — catching factories that close over mutable
 * state or otherwise diverge across calls. The first array is returned;
 * the second is discarded after the comparison. `create_app_server`
 * invokes the factory again per-test with its real ctx, and those are
 * the handlers that actually serve requests.
 *
 * Safe as long as the factory is pure with respect to the endpoint `path`
 * and the action `spec.method` list — the canonical helpers
 * (`create_standard_rpc_actions`, `create_admin_actions`, `create_account_actions`,
 * etc.) are. Factories that return a different `path` based on `ctx` will
 * produce a setup/runtime mismatch; the path-purity assert below surfaces
 * that as a clear `gro check` error rather than a silent test/runtime drift.
 *
 * @throws Error if the factory's two stub-ctx invocations produce different
 *   `(path, method-list)` shapes — surfaces non-pure factories at setup time.
 */
export const resolve_rpc_endpoints_for_setup = (rpc_endpoints, session_options) => {
    if (typeof rpc_endpoints !== 'function')
        return rpc_endpoints;
    const first = rpc_endpoints(create_stub_app_server_context(session_options));
    const second = rpc_endpoints(create_stub_app_server_context(session_options));
    const summarize = (eps) => JSON.stringify(eps
        .map((ep) => ({
        path: ep.path,
        methods: ep.actions.map((a) => a.spec.method).sort(),
    }))
        .sort((a, b) => a.path.localeCompare(b.path)));
    const summary_a = summarize(first);
    const summary_b = summarize(second);
    if (summary_a !== summary_b) {
        throw new Error('rpc_endpoints factory is not path-pure: two invocations with equivalent stub ctxs produced different (path, method) shapes. ' +
            `The factory must be pure wrt endpoint path and action method list — see ../testing/rpc_helpers.ts. ` +
            `first=${summary_a} second=${summary_b}`);
    }
    return first;
};
/**
 * Create a `RequestInit` for a JSON-RPC POST request.
 *
 * @param params - params (omit for parameterless methods; `null` is also
 *                stripped for ergonomic call sites — JSON-RPC 2.0 §4.2
 *                forbids `"params": null` on the wire, and `create_rpc_endpoint`
 *                rejects `z.null()` action input schemas at registration).
 *                Tests that need to construct a literal `"params": null`
 *                envelope (e.g. asserting envelope-level rejection) should
 *                build the body inline rather than route through this helper.
 * @param id - request id (default `'test'`)
 */
export const create_rpc_post_init = (method, params, id = 'test') => {
    const envelope = { jsonrpc: JSONRPC_VERSION, method, id };
    if (params !== undefined && params !== null)
        envelope.params = params;
    return {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(envelope),
    };
};
/**
 * Build a GET URL with JSON-RPC query parameters.
 *
 * @param endpoint_path - the RPC endpoint path (e.g., `/api/rpc`)
 * @param params - params (omit for parameterless methods)
 * @param id - request id (default `'test'`)
 */
export const create_rpc_get_url = (endpoint_path, method, params, id = 'test') => {
    const search = new URLSearchParams({ method, id: String(id) });
    if (params !== undefined && params !== null) {
        search.set('params', JSON.stringify(params));
    }
    return `${endpoint_path}?${search.toString()}`;
};
/**
 * Assert that a response body is a valid JSON-RPC error response.
 *
 * Validates the structure matches `JsonrpcErrorResponse` and optionally
 * checks the error code.
 *
 * @param expected_code - optional error code to assert
 */
export const assert_jsonrpc_error_response = (body, expected_code) => {
    const result = JsonrpcErrorResponse.safeParse(body);
    assert.ok(result.success, `not a valid JSON-RPC error response: ${JSON.stringify(body)}`);
    if (expected_code !== undefined) {
        assert.strictEqual(result.data.error.code, expected_code, `expected error code ${expected_code}, got ${result.data.error.code}`);
    }
};
/**
 * Assert that a response body is a valid JSON-RPC success response.
 *
 * Validates the structure matches `JsonrpcResponse`. When `output_schema`
 * is provided, also validates the `result` field against the declared
 * output schema — matching the REST round-trip's `assert_response_matches_spec`.
 *
 * @param output_schema - optional Zod schema to validate the `result` field against
 */
export const assert_jsonrpc_success_response = (body, output_schema) => {
    const result = JsonrpcResponse.safeParse(body);
    assert.ok(result.success, `not a valid JSON-RPC success response: ${JSON.stringify(body)}`);
    if (output_schema) {
        const output_result = output_schema.safeParse(result.data.result);
        if (!output_result.success) {
            assert.fail(`JSON-RPC result does not match output schema: ${JSON.stringify(output_result.error.issues)}`);
        }
    }
};
/** Adapt a `Hono`-style app into an `RpcTestTransport`. */
export const http_transport = (app) => async (url, init) => app.request(url, init);
/** Base default headers merged into every `rpc_call` request. */
const RPC_CALL_DEFAULT_HEADERS_BASE = {
    host: 'localhost',
    'Content-Type': 'application/json',
};
/** Default headers merged into every `rpc_call` request. Includes `origin`. */
const RPC_CALL_DEFAULT_HEADERS = {
    ...RPC_CALL_DEFAULT_HEADERS_BASE,
    origin: 'http://localhost:5173',
};
/**
 * One-shot JSON-RPC call over a Hono app.
 *
 * Merges sensible defaults (`host`, `origin`, `Content-Type`) under
 * caller-provided headers, fires POST (default) or GET, parses the envelope,
 * and returns a discriminated result.
 *
 * @throws Error if the response body is neither a valid `JsonrpcResponse`
 *   nor `JsonrpcErrorResponse` envelope — protocol-level failures the caller
 *   should never tolerate. All JSON-RPC errors come back via `{ok: false, error}`
 *   so assertions can focus on `error.code` / `error.data.reason`.
 */
export const rpc_call = async (args) => {
    const { app, path, method, params, headers, id = 'test', verb = 'POST', suppress_default_origin, } = args;
    const defaults = suppress_default_origin
        ? RPC_CALL_DEFAULT_HEADERS_BASE
        : RPC_CALL_DEFAULT_HEADERS;
    let url;
    let init;
    if (verb === 'GET') {
        url = create_rpc_get_url(path, method, params, id);
        init = { method: 'GET', headers: { ...defaults, ...headers } };
    }
    else {
        url = path;
        const post = create_rpc_post_init(method, params, id);
        init = {
            method: 'POST',
            headers: {
                ...defaults,
                ...post.headers,
                ...headers,
            },
            body: post.body,
        };
    }
    const res = await (typeof app === 'function' ? app(url, init) : app.request(url, init));
    const status = res.status;
    const body = await res.json();
    const success = JsonrpcResponse.safeParse(body);
    if (success.success) {
        return { ok: true, status, result: success.data.result };
    }
    const error = JsonrpcErrorResponse.safeParse(body);
    if (error.success) {
        return {
            ok: false,
            status,
            error: {
                code: error.data.error.code,
                message: error.data.error.message,
                data: error.data.error.data,
            },
        };
    }
    throw new Error(`rpc_call: response is not a valid JSON-RPC envelope (method=${method}, status=${status}): ${JSON.stringify(body)}`);
};
/**
 * Same as `rpc_call` but without the default `origin` header. Use for
 * bearer-auth probes: `bearer_auth` discards the token when Origin or
 * Referer is present (browser context), so a bearer probe via `rpc_call`
 * would short-circuit to 401 before the token is ever validated.
 *
 * Equivalent to `rpc_call({...args, suppress_default_origin: true})`.
 */
export const rpc_call_non_browser = (args) => rpc_call({ ...args, suppress_default_origin: true });
/**
 * Typed wrapper over `rpc_call` — binds `params` to `z.infer<spec.input>`
 * and the success `result` to `z.infer<spec.output>` via the generic.
 *
 * Success results are validated at runtime against `spec.output` (same
 * contract as `rpc_call_typed`); a mismatch throws. Error responses come
 * back on the discriminated `{ok: false, error}` branch — use this for
 * happy-path + denial-path assertions where the error `data.reason` shape
 * is still asserted manually. For adversarial input tests that send
 * malformed params, use the untyped `rpc_call`.
 *
 * @throws Error if the success `result` does not parse against `spec.output`,
 *   or if `rpc_call` itself throws on an envelope violation.
 */
export const rpc_call_for_spec = async (args) => {
    const { spec, params, ...rest } = args;
    const res = await rpc_call({ ...rest, method: spec.method, params });
    if (!res.ok) {
        return res;
    }
    const parsed = spec.output.safeParse(res.result);
    if (!parsed.success) {
        throw new Error(`rpc_call_for_spec(${spec.method}) result did not match spec.output: ${JSON.stringify(parsed.error.issues)}`);
    }
    return { ok: true, status: res.status, result: parsed.data };
};
/**
 * Same as `rpc_call` but parses the success `result` through the given
 * output schema and returns typed data. Envelope-level failures or error
 * responses throw — use the untyped `rpc_call` for tests that need to
 * assert on specific error shapes.
 *
 * @throws Error if the response is a JSON-RPC error, if `rpc_call` throws
 *   on an envelope violation, or if the result fails `output_schema.safeParse`.
 */
export const rpc_call_typed = async (args, output_schema) => {
    const res = await rpc_call(args);
    if (!res.ok) {
        throw new Error(`rpc_call_typed(${args.method}) returned error: code=${res.error.code} message=${res.error.message} data=${JSON.stringify(res.error.data)}`);
    }
    const parsed = output_schema.safeParse(res.result);
    if (!parsed.success) {
        throw new Error(`rpc_call_typed(${args.method}) result did not match output schema: ${JSON.stringify(parsed.error.issues)}`);
    }
    return parsed.data;
};
// -- registry/surface lookup helpers -----------------------------------------
/**
 * Find the `RpcAction` for a method within a set of RPC endpoint specs.
 * Returns both the endpoint path and the matched action. `undefined` when
 * the method is not registered.
 */
export const find_rpc_action = (rpc_endpoints, method) => {
    for (const ep of rpc_endpoints) {
        for (const action of ep.actions) {
            if (action.spec.method === method)
                return { path: ep.path, action };
        }
    }
    return undefined;
};
/**
 * Find the generated surface entry for a method — the shape returned by
 * `generate_app_surface` (JSON-serializable, useful for schema assertions
 * at the boundary of a consumer test).
 */
export const find_rpc_method = (rpc_endpoints, method) => {
    for (const ep of rpc_endpoints) {
        for (const method_spec of ep.methods) {
            if (method_spec.name === method)
                return { path: ep.path, method_spec };
        }
    }
    return undefined;
};
/**
 * Resolve a single RPC endpoint path — the common case where a consumer
 * mounts exactly one `create_rpc_endpoint`.
 *
 * Used at suite setup time to hard-fail integration suites (admin / audit /
 * SSE / rate-limiting) when the consumer omitted `rpc_endpoints` rather
 * than letting tests fail mid-run with confusing errors.
 *
 * Callers that need multi-endpoint support should iterate `rpc_endpoints`
 * directly.
 *
 * @throws Error if `rpc_endpoints` is empty (hard-fail; see the suite options
 *   docs) or has more than one entry (ambiguous).
 */
export const require_rpc_endpoint_path = (rpc_endpoints) => {
    if (rpc_endpoints.length === 0) {
        throw new Error('rpc_endpoints is empty — the admin/audit integration suites require an RPC endpoint. Pass `rpc_endpoints` on the suite options.');
    }
    if (rpc_endpoints.length > 1) {
        throw new Error(`rpc_endpoints has ${rpc_endpoints.length} entries; this helper expects exactly one. Iterate rpc_endpoints manually for multi-endpoint setups.`);
    }
    return rpc_endpoints[0].path;
};
