/**
 * Resolver SDK tests — interface contract + registry + 2 reference builtins.
 *
 * No network. url_reachable is tested via global fetch mock; x_handle_to_tweet
 * via mocked fetch + env. Real-network E2E (if any) lives in test/e2e/.
 */

import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import {
  ResolverRegistry,
  ResolverError,
  getDefaultRegistry,
  _resetDefaultRegistry,
} from '../src/core/resolvers/index.ts';
import type {
  Resolver,
  ResolverContext,
  ResolverRequest,
  ResolverResult,
} from '../src/core/resolvers/index.ts';
import { urlReachableResolver, checkDnsRebinding } from '../src/core/resolvers/builtin/url-reachable.ts';
import { xHandleToTweetResolver, computeBackoffMs } from '../src/core/resolvers/builtin/x-api/handle-to-tweet.ts';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeCtx(overrides: Partial<ResolverContext> = {}): ResolverContext {
  return {
    config: {},
    logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
    requestId: 'test',
    remote: false,
    ...overrides,
  };
}

// Tiny fake resolver for contract tests
const echoResolver: Resolver<{ v: string }, { v: string }> = {
  id: 'echo',
  cost: 'free',
  backend: 'local',
  description: 'Echo',
  async available() { return true; },
  async resolve(req: ResolverRequest<{ v: string }>): Promise<ResolverResult<{ v: string }>> {
    return {
      value: { v: req.input.v },
      confidence: 1,
      source: 'local',
      fetchedAt: new Date(),
    };
  },
};

// ---------------------------------------------------------------------------
// Registry tests
// ---------------------------------------------------------------------------

describe('ResolverRegistry', () => {
  let reg: ResolverRegistry;

  beforeEach(() => {
    reg = new ResolverRegistry();
  });

  test('starts empty', () => {
    expect(reg.size()).toBe(0);
    expect(reg.list()).toEqual([]);
  });

  test('register + get + has', () => {
    reg.register(echoResolver);
    expect(reg.size()).toBe(1);
    expect(reg.has('echo')).toBe(true);
    expect(reg.get('echo').id).toBe('echo');
  });

  test('register rejects duplicate id', () => {
    reg.register(echoResolver);
    expect(() => reg.register(echoResolver)).toThrow(ResolverError);
    try {
      reg.register(echoResolver);
    } catch (e) {
      expect((e as ResolverError).code).toBe('already_registered');
    }
  });

  test('register rejects empty id', () => {
    expect(() => reg.register({ ...echoResolver, id: '' })).toThrow(ResolverError);
  });

  test('get throws not_found for unknown id', () => {
    try {
      reg.get('nope');
      throw new Error('should have thrown');
    } catch (e) {
      expect(e).toBeInstanceOf(ResolverError);
      expect((e as ResolverError).code).toBe('not_found');
    }
  });

  test('list returns summaries sorted by id', () => {
    reg.register(echoResolver);
    reg.register({ ...echoResolver, id: 'alpha' });
    const list = reg.list();
    expect(list.map(r => r.id)).toEqual(['alpha', 'echo']);
    expect(list[0].cost).toBe('free');
    expect(list[0].backend).toBe('local');
  });

  test('list filters by cost', () => {
    reg.register(echoResolver); // free
    reg.register({ ...echoResolver, id: 'paid-one', cost: 'paid' });
    expect(reg.list({ cost: 'paid' }).map(r => r.id)).toEqual(['paid-one']);
    expect(reg.list({ cost: 'free' }).map(r => r.id)).toEqual(['echo']);
  });

  test('list filters by backend', () => {
    reg.register(echoResolver);
    reg.register({ ...echoResolver, id: 'x-one', backend: 'x-api-v2' });
    expect(reg.list({ backend: 'x-api-v2' }).map(r => r.id)).toEqual(['x-one']);
  });

  test('resolve returns result', async () => {
    reg.register(echoResolver);
    const r = await reg.resolve('echo', { v: 'hi' }, makeCtx());
    expect(r.value).toEqual({ v: 'hi' });
    expect(r.confidence).toBe(1);
  });

  test('resolve throws not_found for unknown id', async () => {
    try {
      await reg.resolve('nope', {}, makeCtx());
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('not_found');
    }
  });

  test('resolve throws unavailable when available() returns false', async () => {
    reg.register({
      ...echoResolver,
      id: 'blocked',
      async available() { return false; },
    });
    try {
      await reg.resolve('blocked', {}, makeCtx());
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('unavailable');
    }
  });

  test('clear empties registry', () => {
    reg.register(echoResolver);
    reg.clear();
    expect(reg.size()).toBe(0);
  });
});

describe('getDefaultRegistry', () => {
  beforeEach(() => _resetDefaultRegistry());
  afterEach(() => _resetDefaultRegistry());

  test('returns a singleton', () => {
    const a = getDefaultRegistry();
    const b = getDefaultRegistry();
    expect(a).toBe(b);
  });

  test('_resetDefaultRegistry gives a fresh instance', () => {
    const a = getDefaultRegistry();
    a.register(echoResolver);
    _resetDefaultRegistry();
    const b = getDefaultRegistry();
    expect(b).not.toBe(a);
    expect(b.size()).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// url_reachable builtin
// ---------------------------------------------------------------------------

describe('url_reachable resolver', () => {
  const originalFetch = globalThis.fetch;

  afterEach(() => {
    globalThis.fetch = originalFetch;
  });

  test('available() is true', async () => {
    expect(await urlReachableResolver.available(makeCtx())).toBe(true);
  });

  test('schema: id + cost + backend match contract', () => {
    expect(urlReachableResolver.id).toBe('url_reachable');
    expect(urlReachableResolver.cost).toBe('free');
    expect(urlReachableResolver.backend).toBe('head-check');
  });

  test('blocks localhost via SSRF guard', async () => {
    const r = await urlReachableResolver.resolve({
      input: { url: 'http://127.0.0.1:1' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(false);
    expect(r.value.reason).toMatch(/internal|private/i);
  });

  test('blocks RFC1918 addresses', async () => {
    const r = await urlReachableResolver.resolve({
      input: { url: 'http://10.0.0.1/' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(false);
  });

  test('blocks AWS metadata endpoint', async () => {
    const r = await urlReachableResolver.resolve({
      input: { url: 'http://169.254.169.254/latest/meta-data/' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(false);
  });

  test('blocks non-http(s) schemes', async () => {
    const r = await urlReachableResolver.resolve({
      input: { url: 'file:///etc/passwd' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(false);
  });

  test('throws schema error for empty url', async () => {
    try {
      await urlReachableResolver.resolve({ input: { url: '' }, context: makeCtx() });
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('schema');
    }
  });

  test('200 response → reachable=true', async () => {
    globalThis.fetch = (async () => new Response('', { status: 200 })) as unknown as typeof fetch;
    const r = await urlReachableResolver.resolve({
      input: { url: 'https://example.com/ok' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(true);
    expect(r.value.status).toBe(200);
  });

  test('404 response → reachable=false with status + reason', async () => {
    globalThis.fetch = (async () => new Response('', { status: 404 })) as unknown as typeof fetch;
    const r = await urlReachableResolver.resolve({
      input: { url: 'https://example.com/dead' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(false);
    expect(r.value.status).toBe(404);
    expect(r.value.reason).toMatch(/HTTP 404/);
  });

  test('HEAD 405 falls back to GET', async () => {
    let callCount = 0;
    globalThis.fetch = (async (_url: string, init?: RequestInit) => {
      callCount++;
      if (init?.method === 'HEAD') return new Response('', { status: 405 });
      return new Response('ok', { status: 200 });
    }) as unknown as typeof fetch;
    const r = await urlReachableResolver.resolve({
      input: { url: 'https://example.com/post-only' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(true);
    expect(callCount).toBe(2);
  });

  test('follows redirect to external URL', async () => {
    const responses = [
      new Response('', { status: 301, headers: { location: 'https://example.org/final' } }),
      new Response('', { status: 200 }),
    ];
    let i = 0;
    globalThis.fetch = (async () => responses[i++]) as unknown as typeof fetch;
    const r = await urlReachableResolver.resolve({
      input: { url: 'https://example.com/redirect' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(true);
    expect(r.value.finalUrl).toBe('https://example.org/final');
  });

  test('blocks redirect to internal URL (per-hop SSRF revalidation)', async () => {
    globalThis.fetch = (async () => new Response('', {
      status: 302,
      headers: { location: 'http://127.0.0.1/admin' },
    })) as unknown as typeof fetch;
    const r = await urlReachableResolver.resolve({
      input: { url: 'https://example.com/redirects-to-local' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(false);
    expect(r.value.reason).toMatch(/redirect to blocked/i);
  });

  test('fetch network failure → reachable=false, confidence=1', async () => {
    globalThis.fetch = (async () => { throw new TypeError('fetch failed'); }) as unknown as typeof fetch;
    const r = await urlReachableResolver.resolve({
      input: { url: 'https://nonexistent.example/' },
      context: makeCtx(),
    });
    expect(r.value.reachable).toBe(false);
    expect(r.value.reason).toMatch(/fetch error/);
    expect(r.confidence).toBe(1);
  });

  test('checkDnsRebinding: skips IP literals', async () => {
    expect(await checkDnsRebinding('http://8.8.8.8/')).toBeNull();
    expect(await checkDnsRebinding('http://127.0.0.1/')).toBeNull();
    expect(await checkDnsRebinding('http://[::1]/')).toBeNull();
  });

  test('checkDnsRebinding: returns null for unparseable URL', async () => {
    expect(await checkDnsRebinding('not a url')).toBeNull();
  });

  test('checkDnsRebinding: returns null on DNS failure (surface via fetch)', async () => {
    // Nonexistent TLD; DNS lookup fails, we let the fetch surface the error.
    const r = await checkDnsRebinding('http://definitely-not-a-real-tld.invalidtld123/');
    expect(r).toBeNull();
  });

  test('AbortSignal fires mid-flight → ResolverError(aborted)', async () => {
    const ac = new AbortController();
    globalThis.fetch = (async () => {
      const err = new Error('aborted');
      err.name = 'AbortError';
      throw err;
    }) as unknown as typeof fetch;
    ac.abort();
    try {
      await urlReachableResolver.resolve({
        input: { url: 'https://example.com/' },
        context: makeCtx({ signal: ac.signal }),
      });
      throw new Error('should have thrown');
    } catch (e) {
      expect(e).toBeInstanceOf(ResolverError);
      expect((e as ResolverError).code).toBe('aborted');
    }
  });
});

// ---------------------------------------------------------------------------
// x_handle_to_tweet builtin
// ---------------------------------------------------------------------------

describe('x_handle_to_tweet resolver', () => {
  const originalFetch = globalThis.fetch;
  const originalToken = process.env.X_API_BEARER_TOKEN;

  beforeEach(() => {
    delete process.env.X_API_BEARER_TOKEN;
  });

  afterEach(() => {
    globalThis.fetch = originalFetch;
    if (originalToken) process.env.X_API_BEARER_TOKEN = originalToken;
    else delete process.env.X_API_BEARER_TOKEN;
  });

  // ---- computeBackoffMs ----

  test('computeBackoffMs: honors Retry-After seconds', () => {
    const r = new Response('', { status: 429, headers: { 'retry-after': '10' } });
    expect(computeBackoffMs(r)).toBe(10_000);
  });

  test('computeBackoffMs: honors Retry-After HTTP-date', () => {
    const now = 1_700_000_000_000; // 2023-11-14T22:13:20Z
    const future = new Date(now + 7_000).toUTCString();
    const r = new Response('', { status: 429, headers: { 'retry-after': future } });
    const ms = computeBackoffMs(r, now);
    expect(ms).toBeGreaterThanOrEqual(6_000);
    expect(ms).toBeLessThanOrEqual(8_000);
  });

  test('computeBackoffMs: honors x-rate-limit-reset epoch seconds', () => {
    const now = 1_700_000_000_000;
    const resetSec = Math.floor(now / 1000) + 15;
    const r = new Response('', { status: 429, headers: { 'x-rate-limit-reset': String(resetSec) } });
    expect(computeBackoffMs(r, now)).toBeGreaterThanOrEqual(14_000);
    expect(computeBackoffMs(r, now)).toBeLessThanOrEqual(16_000);
  });

  test('computeBackoffMs: takes MAX when both headers present', () => {
    const now = 1_700_000_000_000;
    const resetSec = Math.floor(now / 1000) + 30;
    const r = new Response('', {
      status: 429,
      headers: { 'retry-after': '5', 'x-rate-limit-reset': String(resetSec) },
    });
    const ms = computeBackoffMs(r, now);
    expect(ms).toBeGreaterThanOrEqual(29_000);
  });

  test('computeBackoffMs: clamps to floor 2s when no headers', () => {
    const r = new Response('', { status: 429 });
    expect(computeBackoffMs(r)).toBeGreaterThanOrEqual(2_000);
  });

  test('computeBackoffMs: clamps to ceiling 60s', () => {
    const now = 1_700_000_000_000;
    const resetSec = Math.floor(now / 1000) + 600; // 10 min
    const r = new Response('', { status: 429, headers: { 'x-rate-limit-reset': String(resetSec) } });
    expect(computeBackoffMs(r, now)).toBeLessThanOrEqual(60_000);
  });

  test('available() false when token missing', async () => {
    expect(await xHandleToTweetResolver.available(makeCtx())).toBe(false);
  });

  test('available() true when token in env', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake-token';
    expect(await xHandleToTweetResolver.available(makeCtx())).toBe(true);
  });

  test('available() true when token in ctx.config', async () => {
    const ctx = makeCtx({ config: { x_api_bearer_token: 'fake-token' } });
    expect(await xHandleToTweetResolver.available(ctx)).toBe(true);
  });

  test('rejects invalid handle (schema)', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    try {
      await xHandleToTweetResolver.resolve({
        input: { handle: 'bad handle with spaces' },
        context: makeCtx(),
      });
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('schema');
    }
  });

  test('rejects handle longer than 15 chars', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    try {
      await xHandleToTweetResolver.resolve({
        input: { handle: 'a'.repeat(16) },
        context: makeCtx(),
      });
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('schema');
    }
  });

  test('throws unavailable when no token at resolve time', async () => {
    try {
      await xHandleToTweetResolver.resolve({
        input: { handle: 'garrytan' },
        context: makeCtx(),
      });
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('unavailable');
    }
  });

  test('zero candidates → confidence 0', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    globalThis.fetch = (async () => new Response(JSON.stringify({ data: [], meta: { result_count: 0 } }), {
      status: 200,
      headers: { 'content-type': 'application/json' },
    })) as unknown as typeof fetch;
    const r = await xHandleToTweetResolver.resolve({
      input: { handle: 'garrytan', keywords: 'nothing matches' },
      context: makeCtx(),
    });
    expect(r.confidence).toBe(0);
    expect(r.value.candidates).toEqual([]);
    expect(r.value.url).toBeUndefined();
  });

  test('single strong match → confidence >= 0.8 (auto-repair bucket)', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    globalThis.fetch = (async () => new Response(JSON.stringify({
      data: [
        { id: '123', text: 'talking about building gbrain today', created_at: '2026-04-18T00:00:00Z' },
      ],
    }), { status: 200, headers: { 'content-type': 'application/json' } })) as unknown as typeof fetch;
    const r = await xHandleToTweetResolver.resolve({
      input: { handle: 'garrytan', keywords: 'building gbrain' },
      context: makeCtx(),
    });
    expect(r.confidence).toBeGreaterThanOrEqual(0.8);
    expect(r.value.url).toBe('https://x.com/garrytan/status/123');
    expect(r.value.tweet_id).toBe('123');
  });

  test('single weak-match → confidence in 0.5-0.8 review range', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    globalThis.fetch = (async () => new Response(JSON.stringify({
      data: [{ id: '1', text: 'something unrelated entirely', created_at: '2026-04-18T00:00:00Z' }],
    }), { status: 200, headers: { 'content-type': 'application/json' } })) as unknown as typeof fetch;
    const r = await xHandleToTweetResolver.resolve({
      input: { handle: 'garrytan', keywords: 'gbrain knowledge runtime specific terms' },
      context: makeCtx(),
    });
    expect(r.confidence).toBeGreaterThanOrEqual(0.5);
    expect(r.confidence).toBeLessThan(0.8);
  });

  test('many ambiguous candidates → confidence < 0.5 (skip bucket)', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    const data = Array.from({ length: 10 }, (_, i) => ({
      id: String(i + 1),
      text: 'short noise text ' + i,
      created_at: '2026-04-18T00:00:00Z',
    }));
    globalThis.fetch = (async () => new Response(JSON.stringify({ data }), {
      status: 200, headers: { 'content-type': 'application/json' },
    })) as unknown as typeof fetch;
    const r = await xHandleToTweetResolver.resolve({
      input: { handle: 'garrytan', keywords: 'completely different signal words unlikely to match' },
      context: makeCtx(),
    });
    expect(r.confidence).toBeLessThan(0.5);
    expect(r.value.candidates.length).toBe(10);
    expect(r.value.url).toBeUndefined(); // gated by >= 0.5
  });

  test('401 → ResolverError(auth)', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    globalThis.fetch = (async () => new Response('unauthorized', { status: 401 })) as unknown as typeof fetch;
    try {
      await xHandleToTweetResolver.resolve({
        input: { handle: 'garrytan' },
        context: makeCtx(),
      });
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('auth');
    }
  });

  test('403 → ResolverError(auth)', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    globalThis.fetch = (async () => new Response('forbidden', { status: 403 })) as unknown as typeof fetch;
    try {
      await xHandleToTweetResolver.resolve({
        input: { handle: 'garrytan' },
        context: makeCtx(),
      });
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('auth');
    }
  });

  test('500 → ResolverError(upstream) with body snippet', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    globalThis.fetch = (async () => new Response('internal err', { status: 500 })) as unknown as typeof fetch;
    try {
      await xHandleToTweetResolver.resolve({
        input: { handle: 'garrytan' },
        context: makeCtx(),
      });
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('upstream');
      expect((e as ResolverError).message).toMatch(/HTTP 500/);
    }
  });

  test('429 retries then surfaces rate_limited', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    let calls = 0;
    globalThis.fetch = (async () => {
      calls++;
      return new Response('rate', { status: 429, headers: { 'retry-after': '0' } });
    }) as unknown as typeof fetch;
    try {
      await xHandleToTweetResolver.resolve({
        input: { handle: 'garrytan' },
        context: makeCtx(),
      });
      throw new Error('should have thrown');
    } catch (e) {
      expect((e as ResolverError).code).toBe('rate_limited');
      expect(calls).toBeGreaterThanOrEqual(3); // initial + 2 retries
    }
  });

  test('strips X operators from keyword input (injection defense)', async () => {
    process.env.X_API_BEARER_TOKEN = 'fake';
    let capturedUrl = '';
    globalThis.fetch = (async (url: string) => {
      capturedUrl = url;
      return new Response(JSON.stringify({ data: [] }), {
        status: 200, headers: { 'content-type': 'application/json' },
      });
    }) as unknown as typeof fetch;
    await xHandleToTweetResolver.resolve({
      input: { handle: 'garrytan', keywords: 'from:evil_user lang:ja to:someone normal words' },
      context: makeCtx(),
    });
    // Decoded query should still have handle but not extra operators.
    // URLSearchParams encodes spaces as '+', so use token-level assertions.
    const params = new URL(capturedUrl).searchParams;
    const query = params.get('query') ?? '';
    expect(query).toContain('from:garrytan');
    expect(query).not.toContain('from:evil_user');
    expect(query).not.toContain('lang:ja');
    expect(query).not.toContain('to:someone');
    expect(query).toContain('normal');
    expect(query).toContain('words');
  });
});

// ---------------------------------------------------------------------------
// Structural array-items guard for resolver inputSchema + outputSchema.
//
// v0.34 fix wave: handle_to_tweet's `candidates` field shipped as bare
// `type: 'array'` without `items` (caught by community PR #910). Same
// bug class as extract_facts.entity_hints, different surface. Resolvers
// don't ride through buildToolDefs so the MCP-side structural guard
// doesn't see them.
//
// This guard walks the builtin resolvers explicitly (NOT via
// getDefaultRegistry(), which starts empty until commands/resolvers.ts
// registers — codex catch). Both inputSchema AND outputSchema are
// checked because resolvers are bidirectional.
// ---------------------------------------------------------------------------

interface SchemaNode {
  type?: unknown;
  properties?: Record<string, SchemaNode>;
  items?: SchemaNode;
  [k: string]: unknown;
}

function findArrayWithoutItems(node: SchemaNode | undefined, path: string[]): string[] {
  const violations: string[] = [];
  if (!node || typeof node !== 'object') return violations;
  if (node.type === 'array') {
    if (!node.items || typeof node.items !== 'object') {
      violations.push(`${path.join('.') || '<root>'} (array missing items)`);
    } else if (!('type' in node.items)) {
      violations.push(`${path.join('.') || '<root>'}.items (items missing type)`);
    } else {
      violations.push(...findArrayWithoutItems(node.items, [...path, 'items']));
    }
  }
  if (node.properties && typeof node.properties === 'object') {
    for (const [k, child] of Object.entries(node.properties)) {
      violations.push(...findArrayWithoutItems(child as SchemaNode, [...path, k]));
    }
  }
  return violations;
}

describe('builtin resolver schemas — structural array-items guard', () => {
  // Explicit-import list. getDefaultRegistry() returns an empty registry
  // until commands/resolvers.ts registers, so walking it would silently
  // pass with zero resolvers visited (codex finding).
  const builtins = [
    { name: 'url_reachable', resolver: urlReachableResolver },
    { name: 'x_handle_to_tweet', resolver: xHandleToTweetResolver },
  ];

  for (const { name, resolver } of builtins) {
    test(`${name}: inputSchema has no bare arrays`, () => {
      const violations = findArrayWithoutItems(
        resolver.inputSchema as SchemaNode | undefined,
        [name, 'inputSchema'],
      );
      expect(violations).toEqual([]);
    });

    test(`${name}: outputSchema has no bare arrays`, () => {
      const violations = findArrayWithoutItems(
        resolver.outputSchema as SchemaNode | undefined,
        [name, 'outputSchema'],
      );
      expect(violations).toEqual([]);
    });
  }

  test('x_handle_to_tweet candidates declares full item shape with required + additionalProperties:false', () => {
    const out = xHandleToTweetResolver.outputSchema as SchemaNode;
    const candidates = (out.properties as Record<string, SchemaNode>).candidates;
    expect(candidates.type).toBe('array');
    expect(candidates.items).toBeDefined();
    const items = candidates.items as SchemaNode;
    expect(items.type).toBe('object');
    expect(items.properties).toBeDefined();
    const props = items.properties as Record<string, SchemaNode>;
    expect(Object.keys(props).sort()).toEqual(
      ['created_at', 'score', 'text', 'tweet_id', 'url'],
    );
    expect((items as { required?: unknown }).required).toEqual(
      ['tweet_id', 'text', 'created_at', 'score', 'url'],
    );
    expect((items as { additionalProperties?: unknown }).additionalProperties).toBe(false);
  });

  test('coverage: at least 2 builtins walked (catches future regression where registry import drops)', () => {
    // Negative guard against codex's "underspecified registry walk" finding:
    // if this list ever empties (e.g. an autoformatter drops the array),
    // the rest of the suite would silently pass.
    expect(builtins.length).toBeGreaterThanOrEqual(2);
  });
});
