import * as dns from 'node:dns/promises';
import {
  SandboxRunner,
  isPrivateOrReservedIp,
  hostFetch,
  hostPlanfiCall,
  planfiCallTimeoutMs,
  computeWallClockMs,
} from './sandbox-runner';

describe('timeout budgets', () => {
  it('planfi.call honours the job timeout instead of the 10s fetch ceiling', () => {
    // Job B do runbook: timeout 300_000ms — a chamada interna pode usar o
    // orçamento inteiro do job, não o teto de 10s do fetch externo.
    expect(planfiCallTimeoutMs(300_000)).toBe(300_000);
  });

  it('planfi.call keeps the 10s default when the job timeout is absent', () => {
    expect(planfiCallTimeoutMs(0)).toBe(10_000);
  });

  it('planfi.call caps at the max configurable job timeout (15 min)', () => {
    expect(planfiCallTimeoutMs(2_000_000)).toBe(900_000);
  });

  it('wall clock keeps the 3x envelope for short jobs', () => {
    expect(computeWallClockMs(30_000)).toBe(90_000);
    expect(computeWallClockMs(60_000)).toBe(120_000);
  });

  it('wall clock never truncates a job timeout above the 120s ceiling', () => {
    expect(computeWallClockMs(300_000)).toBe(300_000);
    expect(computeWallClockMs(900_000)).toBe(900_000);
  });
});

describe('SandboxRunner', () => {
  let runner: SandboxRunner;

  beforeEach(() => {
    runner = new SandboxRunner();
  });

  // (a) return value + log capture
  it('captures return value and log lines', async () => {
    const result = await runner.run({
      code: `
        log('hello', 'world');
        log('count', 42);
        return { doubled: input.x * 2 };
      `,
      input: { x: 21 },
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('SUCCESS');
    expect(result.output).toEqual({ doubled: 42 });
    expect(result.logs).toHaveLength(2);
    expect(result.logs[0]).toBe('hello world');
    expect(result.logs[1]).toBe('count 42');
    expect(result.durationMs).toBeGreaterThanOrEqual(0);
  });

  // (a2) object/array log args are serialized as JSON, not "[object Object]"
  it('serializes object and array log args as JSON', async () => {
    const result = await runner.run({
      code: `
        log('result', { ok: true, count: 2 });
        log([1, 2, 3]);
        log('mixed', null, undefined, 42);
        return null;
      `,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('SUCCESS');
    expect(result.logs[0]).toBe('result {"ok":true,"count":2}');
    expect(result.logs[1]).toBe('[1,2,3]');
    expect(result.logs[2]).toBe('mixed null undefined 42');
  });

  // (b) timeout on infinite loop → TIMED_OUT
  it('returns TIMED_OUT on infinite loop', async () => {
    const result = await runner.run({
      code: `while(true){}`,
      limits: { timeoutMs: 300, memoryMb: 64 },
    });

    expect(result.status).toBe('TIMED_OUT');
    expect(result.error).toBeDefined();
    // isolated-vm throws the canonical message for genuine timeouts
    expect(result.error?.message).toMatch(/Script execution timed out/);
  }, 5000);

  // (c) thrown error → FAILED with message (stack may be undefined for isolate errors)
  it('returns FAILED with message on thrown error', async () => {
    const result = await runner.run({
      code: `throw new Error('boom');`,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('FAILED');
    expect(result.error).toBeDefined();
    expect(result.error?.message).toContain('boom');
    // stack is present when the host Error carries it, absent for isolate-side errors
    expect(
      typeof result.error?.stack === 'string' ||
        result.error?.stack === undefined,
    ).toBe(true);
  });

  // (d) forbidden globals: process, require, fetch are undefined
  it('does not expose process, require, or fetch globals', async () => {
    const result = await runner.run({
      code: `
        return {
          hasProcess: typeof process,
          hasRequire: typeof require,
          hasFetch: typeof fetch,
        };
      `,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('SUCCESS');
    expect(result.output).toEqual({
      hasProcess: 'undefined',
      hasRequire: 'undefined',
      hasFetch: 'undefined',
    });
  });

  // (e) log truncation cap respected
  it('truncates logs when total size exceeds 256 KB cap', async () => {
    // Each line is ~1024 bytes, 300 lines = ~300 KB > 256 KB cap
    const result = await runner.run({
      code: `
        const big = 'x'.repeat(1024);
        for (let i = 0; i < 300; i++) { log(big); }
        return 'done';
      `,
      limits: { timeoutMs: 5000, memoryMb: 64 },
    });

    expect(result.status).toBe('SUCCESS');
    // Logs should be cut before 300 lines and end with truncation marker
    const lastLog = result.logs[result.logs.length - 1];
    expect(lastLog).toContain('truncado');
    // Total bytes of actual log lines (excluding marker) should be ≤ 256 KB
    const totalBytes = result.logs
      .filter((l) => !l.includes('truncado'))
      .reduce((acc, l) => acc + Buffer.byteLength(l, 'utf8'), 0);
    expect(totalBytes).toBeLessThanOrEqual(256 * 1024);
  }, 10000);

  // (f) memoryLimit — allocating huge array should fail without crashing host
  it('handles memory exhaustion without crashing the host', async () => {
    const result = await runner.run({
      code: `
        // Try to allocate ~200 MB inside a 32 MB isolate
        const arr = [];
        for (let i = 0; i < 200_000_000; i++) { arr.push(i); }
        return arr.length;
      `,
      limits: { timeoutMs: 5000, memoryMb: 32 },
    });

    // Either FAILED (heap OOM) or TIMED_OUT — host must not crash
    expect(['FAILED', 'TIMED_OUT']).toContain(result.status);
  }, 10000);

  // extra: top-level await works inside user code
  it('supports top-level await in user code', async () => {
    const result = await runner.run({
      code: `
        const val = await Promise.resolve(99);
        return val;
      `,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('SUCCESS');
    expect(result.output).toBe(99);
  });

  // extra: null input is handled gracefully
  it('handles undefined input gracefully', async () => {
    const result = await runner.run({
      code: `return input;`,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('SUCCESS');
    expect(result.output).toBeNull();
  });

  // SBX-1: a job with no explicit return must SUCCEED with undefined/null output.
  // Previously truncateJson(undefined) -> JSON.stringify(undefined) === undefined
  // -> .length threw a TypeError, wrongly marking the run FAILED.
  it('returns SUCCESS with no output when the job has no explicit return', async () => {
    const result = await runner.run({
      code: `
        log('side effect only');
        const x = 1 + 1;
      `,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('SUCCESS');
    expect(result.error).toBeUndefined();
    expect(result.output ?? null).toBeNull();
    expect(result.logs).toContain('side effect only');
  });

  // SBX-1: an explicit `return undefined;` must also SUCCEED (not FAILED).
  it('returns SUCCESS when the job explicitly returns undefined', async () => {
    const result = await runner.run({
      code: `return undefined;`,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('SUCCESS');
    expect(result.error).toBeUndefined();
    expect(result.output ?? null).toBeNull();
  });

  // SBX-2 / SJOB-SEC-02: a legitimate user error whose message contains 'time'
  // (e.g. 'runtime', 'datetime', 'timestamp') must be FAILED with a stack —
  // NOT misclassified as TIMED_OUT by a generic /time/ regex.
  it('classifies a fast user error mentioning "time" as FAILED, not TIMED_OUT', async () => {
    const result = await runner.run({
      code: `throw new Error('invalid datetime at runtime: timestamp parse failed');`,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('FAILED');
    expect(result.error).toBeDefined();
    expect(result.error?.message).toContain('datetime');
    expect(result.error?.message).toContain('runtime');
    expect(result.error?.message).toContain('timestamp');
    // stack is present when the host Error carries it, absent for isolate-side
    // errors — same documented behavior as the plain thrown-error test above.
    expect(
      typeof result.error?.stack === 'string' ||
        result.error?.stack === undefined,
    ).toBe(true);
  });

  // SBX-3: partial logs emitted before a thrown error must be preserved
  // in the FAILED result (previously branches returned logs: []).
  it('preserves partial logs in a FAILED result', async () => {
    const result = await runner.run({
      code: `
        log('before the error');
        log('still here');
        throw new Error('boom');
      `,
      limits: { timeoutMs: 3000, memoryMb: 64 },
    });

    expect(result.status).toBe('FAILED');
    expect(result.logs).toContain('before the error');
    expect(result.logs).toContain('still here');
  });

  // SBX-3: partial logs emitted before a timeout must be preserved
  // in the TIMED_OUT result.
  it('preserves partial logs in a TIMED_OUT result', async () => {
    const result = await runner.run({
      code: `
        log('emitted before timeout');
        while (true) {}
      `,
      limits: { timeoutMs: 300, memoryMb: 64 },
    });

    expect(result.status).toBe('TIMED_OUT');
    expect(result.logs).toContain('emitted before timeout');
  }, 5000);

  // SBX-4: memoryMb < 8 (isolated-vm's hard floor) must NOT reject run().
  // The floor of 8 MB is applied so the Isolate constructs successfully and
  // the run resolves with a normal SandboxResult.
  it('does not reject when memoryMb is below the isolated-vm floor', async () => {
    await expect(
      runner.run({
        code: `return input.x + 1;`,
        input: { x: 41 },
        limits: { timeoutMs: 3000, memoryMb: 1 },
      }),
    ).resolves.toMatchObject({ status: 'SUCCESS', output: 42 });
  });

  // SBX-4: memoryMb = 0 must also be coerced to the floor and resolve.
  it('coerces memoryMb of 0 to the floor and resolves with a SandboxResult', async () => {
    const result = await runner.run({
      code: `return 'ok';`,
      limits: { timeoutMs: 3000, memoryMb: 0 },
    });

    expect(['SUCCESS', 'FAILED', 'TIMED_OUT']).toContain(result.status);
    expect(result.status).toBe('SUCCESS');
    expect(result.output).toBe('ok');
  });

  // -------------------------------------------------------------------------
  // P2: read-only env
  // -------------------------------------------------------------------------

  describe('P2 env', () => {
    it('injects env and makes it readable from user code', async () => {
      const result = await runner.run({
        code: `return { api: env.API_BASE, key: env.SECRET };`,
        env: { API_BASE: 'https://api.example.com', SECRET: 's3cr3t' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toEqual({
        api: 'https://api.example.com',
        key: 's3cr3t',
      });
    });

    it('mutation of env inside the isolate does not affect the host (frozen copy)', async () => {
      const hostEnv = { A: '1' };
      const result = await runner.run({
        code: `
          let threw = false;
          try { env.A = 'tampered'; } catch (e) { threw = true; }
          // delete should also be a no-op on a frozen object
          let deleted = false;
          try { delete env.A; } catch (e) {}
          return { value: env.A, threw, frozen: Object.isFrozen(env) };
        `,
        env: hostEnv,
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ value: '1', frozen: true });
      // Host object is untouched — env is a copy, never a live reference.
      expect(hostEnv).toEqual({ A: '1' });
    });

    it('exposes env as an empty object when none is provided', async () => {
      const result = await runner.run({
        code: `return { type: typeof env, keys: Object.keys(env).length };`,
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toEqual({ type: 'object', keys: 0 });
    });
  });

  // -------------------------------------------------------------------------
  // P2: SSRF range check (unit)
  // -------------------------------------------------------------------------

  describe('isPrivateOrReservedIp', () => {
    it('flags private/loopback/link-local IPv4 ranges', () => {
      expect(isPrivateOrReservedIp('10.0.0.1')).toBe(true);
      expect(isPrivateOrReservedIp('172.16.0.1')).toBe(true);
      expect(isPrivateOrReservedIp('172.31.255.255')).toBe(true);
      expect(isPrivateOrReservedIp('192.168.1.1')).toBe(true);
      expect(isPrivateOrReservedIp('127.0.0.1')).toBe(true);
      expect(isPrivateOrReservedIp('169.254.1.1')).toBe(true);
      expect(isPrivateOrReservedIp('0.0.0.0')).toBe(true);
    });

    it('allows public IPv4 addresses', () => {
      expect(isPrivateOrReservedIp('8.8.8.8')).toBe(false);
      expect(isPrivateOrReservedIp('1.1.1.1')).toBe(false);
      expect(isPrivateOrReservedIp('172.32.0.1')).toBe(false); // just outside /12
      expect(isPrivateOrReservedIp('11.0.0.1')).toBe(false);
    });

    it('flags loopback/ULA/link-local IPv6 and allows public IPv6', () => {
      expect(isPrivateOrReservedIp('::1')).toBe(true);
      expect(isPrivateOrReservedIp('fc00::1')).toBe(true);
      expect(isPrivateOrReservedIp('fd12:3456::1')).toBe(true);
      expect(isPrivateOrReservedIp('fe80::1')).toBe(true);
      expect(isPrivateOrReservedIp('::ffff:127.0.0.1')).toBe(true);
      expect(isPrivateOrReservedIp('2001:4860:4860::8888')).toBe(false);
    });

    // FIX-7: bracket-wrapped IPv6 literals must be classified as private.
    it('strips brackets from IPv6 literals before classifying (FIX-7)', () => {
      expect(isPrivateOrReservedIp('[::1]')).toBe(true);
      expect(isPrivateOrReservedIp('[fe80::1]')).toBe(true);
      expect(isPrivateOrReservedIp('[fc00::1]')).toBe(true);
    });

    // FIX-7: IPv4-mapped hex form ::ffff:7f00:0001 → 127.0.0.1 (private).
    it('classifies IPv4-mapped IPv6 in hex form as private (FIX-7)', () => {
      // ::ffff:7f00:0001 = ::ffff:127.0.0.1
      expect(isPrivateOrReservedIp('::ffff:7f00:1')).toBe(true);
      // ::ffff:c0a8:0101 = ::ffff:192.168.1.1
      expect(isPrivateOrReservedIp('::ffff:c0a8:101')).toBe(true);
      // ::ffff:0808:0808 = ::ffff:8.8.8.8 (public)
      expect(isPrivateOrReservedIp('::ffff:808:808')).toBe(false);
    });
  });

  // -------------------------------------------------------------------------
  // P2: fetch allowlist + SSRF guard
  // -------------------------------------------------------------------------

  describe('P2 fetch', () => {
    const realFetch = global.fetch;

    afterEach(() => {
      global.fetch = realFetch;
      jest.restoreAllMocks();
    });

    /**
     * Build a mock Response-like object for a redirect.
     * fetch() with redirect:'manual' receives an opaque redirect response
     * (status 3xx, Location header set).
     */
    function mockFetchRedirect(status: number, location: string): jest.Mock {
      const headers = new Map<string, string>([['location', location]]);
      return jest.fn().mockResolvedValue({
        ok: false,
        status,
        statusText: 'Found',
        headers: {
          has: (k: string) => headers.has(k.toLowerCase()),
          get: (k: string) => headers.get(k.toLowerCase()) ?? null,
          forEach: (_cb: (v: string, k: string) => void) => {},
        },
        body: null,
      });
    }

    function mockFetchOk(body: string): void {
      // Each call to the mock returns a FRESH ReadableStream (streams are
      // single-use; reusing one would cause the reader to see an already-
      // closed stream and return empty body on the second invocation).
      const encoder = new TextEncoder();
      global.fetch = jest.fn().mockImplementation(() => {
        const bytes = encoder.encode(body);
        const stream = new ReadableStream({
          start(controller) {
            controller.enqueue(bytes);
            controller.close();
          },
        });
        return Promise.resolve({
          ok: true,
          status: 200,
          statusText: 'OK',
          headers: {
            has: (_k: string) => false,
            get: (_k: string) => null,
            forEach: (cb: (v: string, k: string) => void) =>
              cb('application/json', 'content-type'),
          },
          body: stream,
        });
      });
    }

    it('resolves a fetch to a domain on the allowlist (dns + fetch mocked)', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
      mockFetchOk('{"hello":"world"}');

      const result = await runner.run({
        code: `
          const res = await fetch('https://api.example.com/data');
          return { ok: res.ok, status: res.status, body: res.body, ct: res.headers['content-type'] };
        `,
        fetchAllowlist: ['api.example.com'],
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({
        ok: true,
        status: 200,
        body: '{"hello":"world"}',
        ct: 'application/json',
      });
    });

    it('matches allowlist case-insensitively', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
      mockFetchOk('ok');

      const result = await runner.run({
        code: `const r = await fetch('https://API.Example.COM/x'); return r.status;`,
        fetchAllowlist: ['api.example.com'],
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toBe(200);
    });

    it('rejects a fetch to a domain NOT on the allowlist (user can catch)', async () => {
      const lookupSpy = jest.spyOn(dns, 'lookup');
      global.fetch = jest.fn();

      const result = await runner.run({
        code: `
          try {
            await fetch('https://evil.example.net/steal');
            return 'NO_THROW';
          } catch (e) {
            return { caught: true, msg: String(e.message || e) };
          }
        `,
        fetchAllowlist: ['api.example.com'],
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ caught: true });
      expect((result.output as { msg: string }).msg).toContain('allowlist');
      // Domain rejected before any DNS resolution / network call.
      expect(lookupSpy).not.toHaveBeenCalled();
      expect(global.fetch).not.toHaveBeenCalled();
    });

    it('rejects when the hostname resolves to a private IP (SSRF, dns mocked → 127.0.0.1)', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '127.0.0.1', family: 4 }]);
      const fetchSpy = jest.fn() as unknown as typeof fetch;
      global.fetch = fetchSpy;

      const result = await runner.run({
        code: `
          try {
            await fetch('https://internal.example.com/admin');
            return 'NO_THROW';
          } catch (e) {
            return { caught: true, msg: String(e.message || e) };
          }
        `,
        fetchAllowlist: ['internal.example.com'],
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ caught: true });
      expect((result.output as { msg: string }).msg).toContain('SSRF');
      // SSRF guard fires before the actual network request.
      expect(fetchSpy).not.toHaveBeenCalled();
    });

    it('blocks every request when the allowlist is empty (fail-closed)', async () => {
      const lookupSpy = jest.spyOn(dns, 'lookup');
      global.fetch = jest.fn();

      const result = await runner.run({
        code: `
          try {
            await fetch('https://api.example.com/data');
            return 'NO_THROW';
          } catch (e) {
            return { caught: true, msg: String(e.message || e) };
          }
        `,
        fetchAllowlist: [],
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ caught: true });
      expect((result.output as { msg: string }).msg).toContain('vazia');
      expect(lookupSpy).not.toHaveBeenCalled();
      expect(global.fetch).not.toHaveBeenCalled();
    });

    it('rejects non-http(s) protocols', async () => {
      const result = await runner.run({
        code: `
          try {
            await fetch('file:///etc/passwd');
            return 'NO_THROW';
          } catch (e) {
            return { caught: true, msg: String(e.message || e) };
          }
        `,
        fetchAllowlist: ['anything'],
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ caught: true });
      expect((result.output as { msg: string }).msg).toContain('protocolo');
    });

    it('keeps fetch undefined when no allowlist is provided (current behavior)', async () => {
      const result = await runner.run({
        code: `return typeof fetch;`,
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toBe('undefined');
    });

    // -----------------------------------------------------------------------
    // FIX-1: Redirect SSRF
    // -----------------------------------------------------------------------

    describe('FIX-1 redirect SSRF', () => {
      /**
       * The primary exploit: an allowlisted host returns a 302 pointing to a
       * private IP. Without the fix, global fetch follows it automatically.
       * With redirect:'manual' + re-validation, the redirect target is blocked.
       */
      it('rejects a 302 redirect to a private IP (redirect SSRF closed)', async () => {
        // First call: allowlisted host returns 302 → internal IP.
        // With redirect:'manual' the response is handed back as-is; we read
        // the Location and re-run the guard, which must reject.
        jest
          .spyOn(dns, 'lookup')
          // First call: initial host resolves to a public IP (passes).
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValueOnce([{ address: '93.184.216.34', family: 4 }]);

        const fetchMock = mockFetchRedirect(302, 'http://127.0.0.1/metadata');
        global.fetch = fetchMock;

        const result = await runner.run({
          code: `
            try {
              await fetch('https://api.example.com/redirect-me');
              return 'NO_THROW';
            } catch (e) {
              return { caught: true, msg: String(e.message || e) };
            }
          `,
          fetchAllowlist: ['api.example.com'],
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        expect(result.output).toMatchObject({ caught: true });
        // The redirect target (127.0.0.1) must have been blocked.
        const msg = (result.output as { msg: string }).msg;
        expect(msg).toMatch(/SSRF|allowlist|bloqueado/);
        // fetch was called ONCE (the initial request); the redirect was NOT followed.
        expect(fetchMock).toHaveBeenCalledTimes(1);
      });

      it('rejects a 302 redirect to a host not on the allowlist', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValueOnce([{ address: '93.184.216.34', family: 4 }]);

        const fetchMock = mockFetchRedirect(
          302,
          'https://evil.attacker.com/steal',
        );
        global.fetch = fetchMock;

        const result = await runner.run({
          code: `
            try {
              await fetch('https://api.example.com/go');
              return 'NO_THROW';
            } catch (e) {
              return { caught: true, msg: String(e.message || e) };
            }
          `,
          fetchAllowlist: ['api.example.com'],
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        expect(result.output).toMatchObject({ caught: true });
        expect((result.output as { msg: string }).msg).toContain('allowlist');
        // fetch was called exactly once (the initial request, not the redirect).
        expect(fetchMock).toHaveBeenCalledTimes(1);
      });

      it('rejects after exceeding the max redirect hops', async () => {
        // Every hop is to an allowlisted, public-IP host so each individual
        // guard passes, but after REDIRECT_MAX_HOPS the chain is cut.
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        // Always redirects to itself.
        const fetchMock = mockFetchRedirect(
          301,
          'https://api.example.com/loop',
        );
        global.fetch = fetchMock;

        const result = await runner.run({
          code: `
            try {
              await fetch('https://api.example.com/start');
              return 'NO_THROW';
            } catch (e) {
              return { caught: true, msg: String(e.message || e) };
            }
          `,
          fetchAllowlist: ['api.example.com'],
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        expect(result.output).toMatchObject({ caught: true });
        expect((result.output as { msg: string }).msg).toContain(
          'redirecionamentos',
        );
      });

      it('follows a safe redirect (same allowlisted host, public IP) successfully', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        const encoder = new TextEncoder();
        const bytes = encoder.encode('{"redirect":"followed"}');
        const stream = new ReadableStream({
          start(c) {
            c.enqueue(bytes);
            c.close();
          },
        });

        const okResponse = {
          ok: true,
          status: 200,
          statusText: 'OK',
          headers: {
            has: (_k: string) => false,
            get: (_k: string) => null,
            forEach: (cb: (v: string, k: string) => void) =>
              cb('application/json', 'content-type'),
          },
          body: stream,
        };

        const fetchMock = jest
          .fn()
          .mockResolvedValueOnce({
            ok: false,
            status: 302,
            statusText: 'Found',
            headers: {
              has: (k: string) => k.toLowerCase() === 'location',
              get: (k: string) =>
                k.toLowerCase() === 'location'
                  ? 'https://api.example.com/final'
                  : null,
              forEach: (_cb: (v: string, k: string) => void) => {},
            },
            body: null,
          })
          .mockResolvedValueOnce(okResponse);

        global.fetch = fetchMock;

        const result = await runner.run({
          code: `
            const r = await fetch('https://api.example.com/start');
            return r.body;
          `,
          fetchAllowlist: ['api.example.com'],
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        expect(result.output).toBe('{"redirect":"followed"}');
        expect(fetchMock).toHaveBeenCalledTimes(2);
      });
    });

    // -----------------------------------------------------------------------
    // FIX-3: wall-clock deadline + fetch budget
    // -----------------------------------------------------------------------

    describe('FIX-3 wall-clock deadline + fetch budget', () => {
      /**
       * A job that repeatedly awaits a never-resolving fetch would bypass the
       * ivm CPU timeout (no CPU is burned during await). The wall-clock timer
       * must dispose the isolate and report TIMED_OUT.
       *
       * We simulate this with a very short timeoutMs and a fetch that hangs
       * (returns a promise that never resolves).
       */
      it('wall-clock deadline aborts a fetch-loop job that burns no CPU', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        // A fetch that never resolves — simulates a hanging connection.
        global.fetch = jest.fn().mockReturnValue(new Promise(() => {}));

        const result = await runner.run({
          code: `
            while (true) {
              await fetch('https://api.example.com/hang');
            }
          `,
          fetchAllowlist: ['api.example.com'],
          limits: { timeoutMs: 200, memoryMb: 64 },
        });

        // Must not hang indefinitely; must resolve as TIMED_OUT or FAILED.
        expect(['TIMED_OUT', 'FAILED']).toContain(result.status);
      }, 5000);

      it('rejects when the per-run fetch count limit is exceeded (FIX-3)', async () => {
        // Call hostFetch directly to test the counter independently.
        const fetchCounter = { count: 50 }; // already at the max
        const concurrencyCounter = { inFlight: 0 };
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
        global.fetch = jest.fn();

        await expect(
          hostFetch(
            'https://api.example.com/x',
            '{}',
            ['api.example.com'],
            3000,
            fetchCounter,
            concurrencyCounter,
          ),
        ).rejects.toThrow(/limite de requisi/);

        // global.fetch must not have been called.
        expect(global.fetch).not.toHaveBeenCalled();
      });

      it('rejects when concurrency limit is exceeded (FIX-3)', async () => {
        const fetchCounter = { count: 0 };
        const concurrencyCounter = { inFlight: 4 }; // already at ceiling
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
        global.fetch = jest.fn();

        await expect(
          hostFetch(
            'https://api.example.com/x',
            '{}',
            ['api.example.com'],
            3000,
            fetchCounter,
            concurrencyCounter,
          ),
        ).rejects.toThrow(/simultâneas/);

        expect(global.fetch).not.toHaveBeenCalled();
        // ARFIX-03: reserve-before-validate must decrement back on rejection so
        // the over-limit attempt does not leak a slot (5 → 4).
        expect(concurrencyCounter.inFlight).toBe(4);
      });
    });

    // -----------------------------------------------------------------------
    // ARFIX-02: abort in-flight host fetches on teardown / wall-clock dispose
    // -----------------------------------------------------------------------

    describe('ARFIX-02 abort in-flight host fetches', () => {
      it('hostFetch rejection on abort cleans the active-controller set and frees the slot', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        // A fetch that resolves only when its AbortSignal fires (simulates a
        // hung connection that the host can cancel via abort()).
        global.fetch = jest
          .fn()
          .mockImplementation((_url, init: RequestInit) => {
            return new Promise((_resolve, reject) => {
              const signal = init.signal as AbortSignal;
              if (signal.aborted) {
                reject(
                  Object.assign(new Error('aborted'), { name: 'AbortError' }),
                );
                return;
              }
              signal.addEventListener('abort', () => {
                reject(
                  Object.assign(new Error('aborted'), { name: 'AbortError' }),
                );
              });
            });
          });

        const fetchCounter = { count: 0 };
        const concurrencyCounter = { inFlight: 0 };
        const activeControllers = new Set<AbortController>();

        const pending = hostFetch(
          'https://api.example.com/hang',
          '{}',
          ['api.example.com'],
          3000,
          fetchCounter,
          concurrencyCounter,
          activeControllers,
        );

        // Let the host function register its controller (after the awaits).
        await new Promise((r) => setTimeout(r, 20));
        expect(activeControllers.size).toBe(1);
        expect(concurrencyCounter.inFlight).toBe(1);

        // Simulate a run-level teardown aborting every in-flight controller.
        for (const controller of activeControllers) {
          controller.abort();
        }

        await expect(pending).rejects.toThrow(/timeout|rede|bloqueado/);
        // finally must have unregistered the controller and freed the slot.
        expect(activeControllers.size).toBe(0);
        expect(concurrencyCounter.inFlight).toBe(0);
      });

      it('run(): a never-resolving fetch loop aborts the in-flight fetch signal on dispose', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        const seenSignals: AbortSignal[] = [];
        // Never resolves on its own; we only observe the signal it received.
        global.fetch = jest
          .fn()
          .mockImplementation((_url, init: RequestInit) => {
            seenSignals.push(init.signal as AbortSignal);
            return new Promise(() => {});
          });

        const result = await runner.run({
          code: `
            while (true) {
              await fetch('https://api.example.com/hang');
            }
          `,
          fetchAllowlist: ['api.example.com'],
          limits: { timeoutMs: 200, memoryMb: 64 },
        });

        expect(['TIMED_OUT', 'FAILED']).toContain(result.status);
        expect(seenSignals.length).toBeGreaterThan(0);
        // The wall-clock dispose / run teardown must have aborted the in-flight
        // fetch's signal (ARFIX-02).
        expect(seenSignals.some((s) => s.aborted)).toBe(true);
      }, 5000);
    });

    // -----------------------------------------------------------------------
    // ARFIX-03: atomic reserve-before-validate concurrency cap
    // -----------------------------------------------------------------------

    describe('ARFIX-03 atomic concurrency reservation', () => {
      it('caps concurrent hostFetch calls at FETCH_MAX_CONCURRENT (no check-then-reserve gap)', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        const concurrencyCounter = { inFlight: 0 };
        const fetchCounter = { count: 0 };
        let observedMax = 0;
        let reachedFetch = 0;
        const releasers: Array<() => void> = [];

        // A controllable slow fetch: it parks until we release it, letting us
        // hold all in-flight calls open simultaneously.
        global.fetch = jest.fn().mockImplementation(() => {
          reachedFetch += 1;
          observedMax = Math.max(observedMax, concurrencyCounter.inFlight);
          return new Promise((resolve) => {
            releasers.push(() => {
              const encoder = new TextEncoder();
              const stream = new ReadableStream({
                start(c) {
                  c.enqueue(encoder.encode('ok'));
                  c.close();
                },
              });
              resolve({
                ok: true,
                status: 200,
                statusText: 'OK',
                headers: {
                  has: () => false,
                  get: () => null,
                  forEach: () => {},
                },
                body: stream,
              });
            });
          });
        });

        // Fire FETCH_MAX_CONCURRENT + 1 = 5 calls concurrently.
        const calls = Array.from({ length: 5 }, () =>
          hostFetch(
            'https://api.example.com/x',
            '{}',
            ['api.example.com'],
            3000,
            fetchCounter,
            concurrencyCounter,
          ).catch((e: Error) => ({ rejected: e.message })),
        );

        // Give the parked fetches time to reserve their slots.
        await new Promise((r) => setTimeout(r, 30));

        // At most FETCH_MAX_CONCURRENT (4) may be in-flight at fetch time.
        expect(observedMax).toBeLessThanOrEqual(4);
        expect(reachedFetch).toBeLessThanOrEqual(4);
        expect(concurrencyCounter.inFlight).toBeLessThanOrEqual(4);

        // Release all parked fetches and collect results.
        for (const release of releasers) release();
        const results = await Promise.all(calls);

        // Exactly one call must have been rejected for exceeding the limit.
        const rejected = results.filter(
          (r): r is { rejected: string } =>
            typeof r === 'object' && r !== null && 'rejected' in r,
        );
        expect(rejected).toHaveLength(1);
        expect(rejected[0].rejected).toMatch(/simultâneas/);

        // All reservations released after settling.
        expect(concurrencyCounter.inFlight).toBe(0);
      }, 5000);

      it('decrements the reservation when a guard throws after reserving (no leak)', async () => {
        // Empty allowlist makes guardUrl throw AFTER the reservation but before
        // any fetch — the finally must release the reserved slot.
        const concurrencyCounter = { inFlight: 0 };
        const fetchCounter = { count: 0 };
        global.fetch = jest.fn();

        await expect(
          hostFetch(
            'https://api.example.com/x',
            '{}',
            [], // empty allowlist → guardUrl throws (fail-closed)
            3000,
            fetchCounter,
            concurrencyCounter,
          ),
        ).rejects.toThrow(/allowlist|vazia/);

        expect(global.fetch).not.toHaveBeenCalled();
        // The reservation must have been rolled back to 0 (no leak).
        expect(concurrencyCounter.inFlight).toBe(0);
      });

      it('hostPlanfiCall decrements the reservation on origin-escape throw (no leak)', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
        global.fetch = jest.fn();

        const concurrencyCounter = { inFlight: 0 };
        const fetchCounter = { count: 0 };

        await expect(
          hostPlanfiCall(
            JSON.stringify({ raw: { path: '//evil.com/x' } }),
            'https://api.planfi.com',
            'pf_sk_key',
            3000,
            fetchCounter,
            concurrencyCounter,
          ),
        ).rejects.toThrow(/escapar|host/);

        expect(global.fetch).not.toHaveBeenCalled();
        expect(concurrencyCounter.inFlight).toBe(0);
      });

      it('hostPlanfiCall over-limit rejects and decrements back (no leak)', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
        global.fetch = jest.fn();

        const concurrencyCounter = { inFlight: 4 }; // already at ceiling
        const fetchCounter = { count: 0 };

        await expect(
          hostPlanfiCall(
            JSON.stringify({ raw: { path: '/api/x' } }),
            'https://api.planfi.com',
            'pf_sk_key',
            3000,
            fetchCounter,
            concurrencyCounter,
          ),
        ).rejects.toThrow(/simultâneas/);

        expect(global.fetch).not.toHaveBeenCalled();
        expect(concurrencyCounter.inFlight).toBe(4);
      });
    });

    // -----------------------------------------------------------------------
    // ARFIX-01: durationMs >= timeoutMs must NOT force TIMED_OUT classification
    // -----------------------------------------------------------------------

    describe('ARFIX-01 timeout classification', () => {
      it('a slow async op then a thrown user error is FAILED, not TIMED_OUT', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        // Fetch resolves after a delay that exceeds timeoutMs in wall-clock
        // terms, but the await yields so the ivm CPU budget is never consumed
        // and the wall-clock timer (3× timeoutMs) does not fire either.
        global.fetch = jest.fn().mockImplementation(() => {
          return new Promise((resolve) => {
            setTimeout(() => {
              const encoder = new TextEncoder();
              const stream = new ReadableStream({
                start(c) {
                  c.enqueue(encoder.encode('ok'));
                  c.close();
                },
              });
              resolve({
                ok: true,
                status: 200,
                statusText: 'OK',
                headers: {
                  has: () => false,
                  get: () => null,
                  forEach: () => {},
                },
                body: stream,
              });
            }, 120);
          });
        });

        const result = await runner.run({
          code: `
            await fetch('https://api.example.com/slow');
            throw new Error('boom after slow io');
          `,
          fetchAllowlist: ['api.example.com'],
          // timeoutMs 50 → wall-clock = 150ms; the 120ms fetch + throw lands
          // after timeoutMs (durationMs >= timeoutMs) but before wall-clock.
          limits: { timeoutMs: 50, memoryMb: 64 },
        });

        expect(result.status).toBe('FAILED');
        expect(result.error?.message).toContain('boom after slow io');
        // durationMs exceeding timeoutMs must NOT have forced TIMED_OUT.
        expect(result.durationMs).toBeGreaterThanOrEqual(50);
      }, 5000);
    });

    // -----------------------------------------------------------------------
    // FIX-4: Response body OOM (streaming cap)
    // -----------------------------------------------------------------------

    describe('FIX-4 streaming body cap', () => {
      it('truncates body via streaming when server returns more than 1 MB', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        // Build a stream that emits 2 MB total in two 1 MB chunks.
        const oneMB = 1024 * 1024;
        const chunk1 = new Uint8Array(oneMB).fill(65); // 'A' x 1MB
        const chunk2 = new Uint8Array(oneMB).fill(66); // 'B' x 1MB
        const stream = new ReadableStream({
          start(controller) {
            controller.enqueue(chunk1);
            controller.enqueue(chunk2);
            controller.close();
          },
        });

        global.fetch = jest.fn().mockResolvedValue({
          ok: true,
          status: 200,
          statusText: 'OK',
          headers: {
            has: (_k: string) => false,
            get: (_k: string) => null,
            forEach: (_cb: (v: string, k: string) => void) => {},
          },
          body: stream,
        });

        const fetchCounter = { count: 0 };
        const concurrencyCounter = { inFlight: 0 };
        const res = await hostFetch(
          'https://api.example.com/big',
          '{}',
          ['api.example.com'],
          3000,
          fetchCounter,
          concurrencyCounter,
        );

        // Body must be truncated to ≤ 1 MB + truncation marker.
        expect(res.body).toContain('truncado');
        // The content before the marker must not exceed 1 MB.
        const markerIdx = res.body.indexOf('…[truncado]');
        const bodyBytes = Buffer.byteLength(
          res.body.slice(0, markerIdx),
          'utf8',
        );
        expect(bodyBytes).toBeLessThanOrEqual(oneMB);
      });

      it('does not truncate body under 1 MB', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        const smallBody = 'hello world';
        const encoder = new TextEncoder();
        const bytes = encoder.encode(smallBody);
        const stream = new ReadableStream({
          start(c) {
            c.enqueue(bytes);
            c.close();
          },
        });

        global.fetch = jest.fn().mockResolvedValue({
          ok: true,
          status: 200,
          statusText: 'OK',
          headers: {
            has: () => false,
            get: () => null,
            forEach: (_cb: (v: string, k: string) => void) => {},
          },
          body: stream,
        });

        const fetchCounter = { count: 0 };
        const concurrencyCounter = { inFlight: 0 };
        const res = await hostFetch(
          'https://api.example.com/small',
          '{}',
          ['api.example.com'],
          3000,
          fetchCounter,
          concurrencyCounter,
        );

        expect(res.body).toBe(smallBody);
        expect(res.body).not.toContain('truncado');
      });
    });

    // -----------------------------------------------------------------------
    // FIX-5: __hostFetch not accessible in isolate global after bootstrap
    // -----------------------------------------------------------------------

    describe('FIX-5 raw __hostFetch not exposed in isolate', () => {
      it('__hostFetch is undefined in the isolate after bootstrap (when fetch enabled)', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
        mockFetchOk('ok');

        // Ask the isolate itself to report typeof __hostFetch.
        const result = await runner.run({
          code: `return typeof globalThis.__hostFetch;`,
          fetchAllowlist: ['api.example.com'],
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        // After the bootstrap IIFE runs, __hostFetch must be gone.
        expect(result.output).toBe('undefined');
      });

      it('__hostFetch is also undefined when fetch is not enabled', async () => {
        const result = await runner.run({
          code: `return typeof globalThis.__hostFetch;`,
          // No fetchAllowlist → fetch not wired → __hostFetch never set.
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        expect(result.output).toBe('undefined');
      });
    });

    // -----------------------------------------------------------------------
    // FIX-6: User init cannot override method/redirect/headers
    // -----------------------------------------------------------------------

    describe('FIX-6 user init sanitization', () => {
      it('restricts method to the allowlist (GET/POST/…); unknown method defaults to GET', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
        mockFetchOk('ok');

        // We cannot easily inspect what method was passed from inside the
        // isolate, so we call hostFetch directly.
        const fetchCounter = { count: 0 };
        const concurrencyCounter = { inFlight: 0 };
        const capturedInits: RequestInit[] = [];
        const origFetch = global.fetch;
        global.fetch = jest
          .fn()
          .mockImplementation((url: string, init: RequestInit) => {
            capturedInits.push(init);
            return origFetch(url, init);
          });

        await hostFetch(
          'https://api.example.com/x',
          JSON.stringify({ method: 'TRACE' }), // disallowed method
          ['api.example.com'],
          3000,
          fetchCounter,
          concurrencyCounter,
        ).catch(() => {});

        if (capturedInits.length > 0) {
          // If fetch was actually called, the method must have been defaulted.
          expect(capturedInits[0].method).toBe('GET');
        }
        // The key assertion: TRACE is not in ALLOWED_METHODS, so it is converted.
        // Whether fetch was called or not (network may fail in test), the method
        // must not be 'TRACE'.
        for (const init of capturedInits) {
          expect(init.method).not.toBe('TRACE');
        }
      });

      it('strips hop-by-hop / forwarding headers from user init (FIX-6)', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
        mockFetchOk('ok');

        const capturedInits: RequestInit[] = [];
        const origFetch = global.fetch;
        global.fetch = jest.fn().mockImplementation((url, init) => {
          capturedInits.push(init as RequestInit);
          return origFetch(url as string, init as RequestInit);
        });

        const fetchCounter = { count: 0 };
        const concurrencyCounter = { inFlight: 0 };
        await hostFetch(
          'https://api.example.com/x',
          JSON.stringify({
            headers: {
              Host: 'evil.internal',
              'X-Forwarded-For': '127.0.0.1',
              'X-Forwarded-Host': 'attacker.com',
              'Content-Type': 'application/json', // safe, must pass through
            },
          }),
          ['api.example.com'],
          3000,
          fetchCounter,
          concurrencyCounter,
        ).catch(() => {});

        if (capturedInits.length > 0) {
          const sentHeaders = capturedInits[0].headers as Record<
            string,
            string
          >;
          // Blocked headers must not be present.
          expect(sentHeaders['Host']).toBeUndefined();
          expect(sentHeaders['X-Forwarded-For']).toBeUndefined();
          expect(sentHeaders['X-Forwarded-Host']).toBeUndefined();
          // Safe header must pass through.
          expect(sentHeaders['Content-Type']).toBe('application/json');
        }
      });

      it('hard-sets redirect:manual regardless of user init (FIX-1/FIX-6)', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        const encoder = new TextEncoder();
        const bytes = encoder.encode('ok');
        const stream = new ReadableStream({
          start(c) {
            c.enqueue(bytes);
            c.close();
          },
        });

        const capturedInits: RequestInit[] = [];
        global.fetch = jest.fn().mockImplementation((url, init) => {
          capturedInits.push(init as RequestInit);
          return Promise.resolve({
            ok: true,
            status: 200,
            statusText: 'OK',
            headers: {
              has: () => false,
              get: () => null,
              forEach: () => {},
            },
            body: stream,
          });
        });

        const fetchCounter = { count: 0 };
        const concurrencyCounter = { inFlight: 0 };
        await hostFetch(
          'https://api.example.com/x',
          JSON.stringify({ redirect: 'follow' }), // user tries to override
          ['api.example.com'],
          3000,
          fetchCounter,
          concurrencyCounter,
        );

        expect(capturedInits.length).toBeGreaterThan(0);
        // redirect must always be 'manual', never what the user requested.
        expect(capturedInits[0].redirect).toBe('manual');
      });
    });

    // -----------------------------------------------------------------------
    // FIX-8: Host errors sanitized before crossing into isolate
    // -----------------------------------------------------------------------

    describe('FIX-8 error sanitization', () => {
      it('does not leak internal stack traces or file paths to user code', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        // Simulate a network error with an internal-looking stack.
        global.fetch = jest.fn().mockRejectedValue(
          Object.assign(new Error('ECONNREFUSED /var/internal/socket'), {
            stack:
              'Error: ECONNREFUSED /var/internal/socket\n    at /opt/app/node_modules/undici/src/fetch.js:42',
          }),
        );

        const result = await runner.run({
          code: `
            try {
              await fetch('https://api.example.com/x');
              return 'ok';
            } catch (e) {
              return { msg: String(e.message || e) };
            }
          `,
          fetchAllowlist: ['api.example.com'],
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        const msg = (result.output as { msg: string }).msg;
        // The internal path and stack must not leak.
        expect(msg).not.toContain('/var/internal/socket');
        expect(msg).not.toContain('/opt/app/node_modules');
        expect(msg).not.toContain('undici');
        // But the user must still get a meaningful error.
        expect(msg).toMatch(/fetch falhou|rede/);
      });
    });
  });

  // -------------------------------------------------------------------------
  // planfi.call
  // -------------------------------------------------------------------------

  describe('planfi.call', () => {
    const realFetch = global.fetch;

    afterEach(() => {
      global.fetch = realFetch;
      jest.restoreAllMocks();
    });

    // -----------------------------------------------------------------------
    // RPC mode (catálogo): planfi.call("customer.list", params)
    // -----------------------------------------------------------------------
    it('modo RPC: customer.list desembrulha data, devolve array e manda a query', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

      const captured: Array<{ url: string; init: RequestInit }> = [];
      const encoder = new TextEncoder();
      global.fetch = jest
        .fn()
        .mockImplementation((url: string, init: RequestInit) => {
          captured.push({ url, init });
          const bytes = encoder.encode(
            '{"data":[{"id":"a"},{"id":"b"}],"total":2}',
          );
          const stream = new ReadableStream({
            start(c) {
              c.enqueue(bytes);
              c.close();
            },
          });
          return Promise.resolve({
            ok: true,
            status: 200,
            statusText: 'OK',
            headers: { has: () => false, get: () => null, forEach: () => {} },
            body: stream,
          });
        });

      const result = await runner.run({
        code: `
          const clientes = await planfi.call("customer.list", { email: "a@b.com" });
          return { n: clientes.length, first: clientes[0].id };
        `,
        env: { PLANFI_KEY: 'pf_sk_testkey' },
        planfi: { baseUrl: 'https://api.planfi.com' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ n: 2, first: 'a' });
      expect(captured).toHaveLength(1);
      expect(captured[0].url).toContain('/api/clients');
      expect(captured[0].url).toContain('email=a%40b.com');
      const h = captured[0].init.headers as Record<string, string>;
      expect(h['X-Planfi-Service-Key']).toBe('pf_sk_testkey');
    });

    it('modo RPC: método desconhecido lança erro claro e não chama fetch', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
      global.fetch = jest.fn();

      const result = await runner.run({
        code: `
          try { await planfi.call("customer.delete", {}); return 'NO_THROW'; }
          catch (e) { return { caught: true, msg: String(e.message || e) }; }
        `,
        env: { PLANFI_KEY: 'pf_sk_testkey' },
        planfi: { baseUrl: 'https://api.planfi.com' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ caught: true });
      expect((result.output as { msg: string }).msg).toContain(
        'método desconhecido',
      );
      expect(global.fetch).not.toHaveBeenCalled();
    });

    it('modo RPC: erro HTTP da API vira throw com a mensagem', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
      const encoder = new TextEncoder();
      global.fetch = jest.fn().mockImplementation(() => {
        const bytes = encoder.encode('{"message":"Service key negada"}');
        const stream = new ReadableStream({
          start(c) {
            c.enqueue(bytes);
            c.close();
          },
        });
        return Promise.resolve({
          ok: false,
          status: 403,
          statusText: 'Forbidden',
          headers: { has: () => false, get: () => null, forEach: () => {} },
          body: stream,
        });
      });

      const result = await runner.run({
        code: `
          try { await planfi.call("customer.list", {}); return 'NO_THROW'; }
          catch (e) { return { caught: true, msg: String(e.message || e) }; }
        `,
        env: { PLANFI_KEY: 'pf_sk_testkey' },
        planfi: { baseUrl: 'https://api.planfi.com' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ caught: true });
      expect((result.output as { msg: string }).msg).toContain(
        'Service key negada',
      );
    });

    // -----------------------------------------------------------------------
    // 1. planfi.call hits the correct URL and attaches X-Planfi-Service-Key
    // -----------------------------------------------------------------------
    it('hits the configured base host with X-Planfi-Service-Key from env.PLANFI_KEY', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

      const capturedRequests: Array<{ url: string; init: RequestInit }> = [];
      const encoder = new TextEncoder();
      global.fetch = jest
        .fn()
        .mockImplementation((url: string, init: RequestInit) => {
          capturedRequests.push({ url, init });
          const bytes = encoder.encode('{"ok":true}');
          const stream = new ReadableStream({
            start(c) {
              c.enqueue(bytes);
              c.close();
            },
          });
          return Promise.resolve({
            ok: true,
            status: 200,
            statusText: 'OK',
            headers: {
              has: () => false,
              get: () => null,
              forEach: () => {},
            },
            body: stream,
          });
        });

      const result = await runner.run({
        code: `
          const res = await planfi.call({ method: 'GET', path: '/api/users' });
          return { ok: res.ok, status: res.status };
        `,
        env: { PLANFI_KEY: 'pf_sk_testkey123' },
        planfi: { baseUrl: 'https://api.planfi.com' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ ok: true, status: 200 });

      // Verify the correct URL was hit and the auth header injected.
      expect(capturedRequests).toHaveLength(1);
      expect(capturedRequests[0].url).toContain('api.planfi.com');
      expect(capturedRequests[0].url).toContain('/api/users');
      const sentHeaders = capturedRequests[0].init.headers as Record<
        string,
        string
      >;
      expect(sentHeaders['X-Planfi-Service-Key']).toBe('pf_sk_testkey123');
    });

    // -----------------------------------------------------------------------
    // 2. planfi.call without PLANFI_KEY rejects with clear error
    // -----------------------------------------------------------------------
    it('rejects with clear error when PLANFI_KEY secret is absent', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

      const result = await runner.run({
        code: `
          try {
            await planfi.call({ path: '/api/data' });
            return 'NO_THROW';
          } catch (e) {
            return { caught: true, msg: String(e.message || e) };
          }
        `,
        env: {}, // no PLANFI_KEY
        planfi: { baseUrl: 'https://api.planfi.com' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ caught: true });
      expect((result.output as { msg: string }).msg).toContain('PLANFI_KEY');
    });

    // -----------------------------------------------------------------------
    // 3. planfi.call cannot escape to another origin
    // -----------------------------------------------------------------------
    it('rejects when path tries to escape to another origin', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
      global.fetch = jest.fn();

      const result = await runner.run({
        code: `
          try {
            await planfi.call({ path: '//evil.com/steal' });
            return 'NO_THROW';
          } catch (e) {
            return { caught: true, msg: String(e.message || e) };
          }
        `,
        env: { PLANFI_KEY: 'pf_sk_testkey' },
        planfi: { baseUrl: 'https://api.planfi.com' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toMatchObject({ caught: true });
      expect((result.output as { msg: string }).msg).toMatch(/escapar|host/);
      expect(global.fetch).not.toHaveBeenCalled();
    });

    // -----------------------------------------------------------------------
    // 4. planfi.call PERMITE base que resolve para IP privado (base confiável).
    //    O guard de IP privado (SSRF) NÃO se aplica ao planfi.call: ele é travado
    //    same-origin na baseUrl que o operador configurou (ex.: core em localhost
    //    no dev). Bloquear isso quebrava o uso local — ver guardUrl allowPrivate.
    // -----------------------------------------------------------------------
    it('allows planfi.call when the base resolves to a private IP (trusted base)', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '127.0.0.1', family: 4 }]);
      const fetchMock = jest.fn().mockResolvedValue({
        ok: true,
        status: 200,
        statusText: 'OK',
        headers: {
          has: () => false,
          get: () => null,
          forEach: (cb: (v: string, k: string) => void) =>
            cb('application/json', 'content-type'),
        },
        body: null,
      });
      global.fetch = fetchMock;

      const result = await runner.run({
        code: `
          try {
            await planfi.call({ path: '/api/data' });
            return { ok: true };
          } catch (e) {
            return { caught: true, msg: String(e.message || e) };
          }
        `,
        env: { PLANFI_KEY: 'pf_sk_testkey' },
        planfi: { baseUrl: 'https://internal.planfi.local' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      // O essencial: NÃO foi bloqueado por SSRF e a request chegou ao fetch.
      expect(result.status).toBe('SUCCESS');
      expect(global.fetch).toHaveBeenCalled();
      const msg = (result.output as { msg?: string }).msg ?? '';
      expect(msg).not.toMatch(/SSRF|privado/);
    });

    // -----------------------------------------------------------------------
    // 5. planfi NOT injected when input.planfi is absent
    // -----------------------------------------------------------------------
    it('does not inject planfi when input.planfi is absent', async () => {
      const result = await runner.run({
        code: `return typeof planfi;`,
        env: { PLANFI_KEY: 'pf_sk_testkey' },
        // no planfi field
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(result.output).toBe('undefined');
    });

    // -----------------------------------------------------------------------
    // 6. User headers cannot override X-Planfi-Service-Key
    // -----------------------------------------------------------------------
    it('user-supplied headers cannot override X-Planfi-Service-Key', async () => {
      jest
        .spyOn(dns, 'lookup')
        // @ts-expect-error overload {all:true} returns array
        .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

      const capturedRequests: Array<{ url: string; init: RequestInit }> = [];
      const encoder = new TextEncoder();
      global.fetch = jest
        .fn()
        .mockImplementation((url: string, init: RequestInit) => {
          capturedRequests.push({ url, init });
          const bytes = encoder.encode('ok');
          const stream = new ReadableStream({
            start(c) {
              c.enqueue(bytes);
              c.close();
            },
          });
          return Promise.resolve({
            ok: true,
            status: 200,
            statusText: 'OK',
            headers: { has: () => false, get: () => null, forEach: () => {} },
            body: stream,
          });
        });

      const result = await runner.run({
        code: `
          const res = await planfi.call({
            path: '/api/test',
            headers: { 'X-Planfi-Service-Key': 'ATTACKER_KEY' },
          });
          return res.status;
        `,
        env: { PLANFI_KEY: 'pf_sk_real_key' },
        planfi: { baseUrl: 'https://api.planfi.com' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
      });

      expect(result.status).toBe('SUCCESS');
      expect(capturedRequests).toHaveLength(1);
      const sentHeaders = capturedRequests[0].init.headers as Record<
        string,
        string
      >;
      // The auth header must be the real key, not the attacker's value.
      expect(sentHeaders['X-Planfi-Service-Key']).toBe('pf_sk_real_key');
    });

    // -----------------------------------------------------------------------
    // hostPlanfiCall unit tests (called directly)
    // -----------------------------------------------------------------------
    describe('hostPlanfiCall direct', () => {
      it('rejects when PLANFI_KEY is empty', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        await expect(
          hostPlanfiCall(
            JSON.stringify({ raw: { path: '/api/x' } }),
            'https://api.planfi.com',
            '', // empty key
            3000,
            { count: 0 },
            { inFlight: 0 },
          ),
        ).rejects.toThrow('PLANFI_KEY');
      });

      it('rejects when path escapes to another origin', async () => {
        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        await expect(
          hostPlanfiCall(
            JSON.stringify({ raw: { path: '//evil.com/x' } }),
            'https://api.planfi.com',
            'pf_sk_key',
            3000,
            { count: 0 },
            { inFlight: 0 },
          ),
        ).rejects.toThrow(/escapar|host/);
      });
    });

    // -----------------------------------------------------------------------
    // T6: planfi.call with system key fallback
    // -----------------------------------------------------------------------
    describe('T6: system key fallback (SCHEDULED_JOBS_INTERNAL_PLANFI_KEY)', () => {
      const originalEnv = process.env;

      afterEach(() => {
        process.env = originalEnv;
      });

      it('uses system key from process.env when job env has no PLANFI_KEY', async () => {
        // Arrange: set the system key in process.env
        process.env = {
          ...originalEnv,
          SCHEDULED_JOBS_INTERNAL_PLANFI_KEY: 'sys_key_123',
        };

        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        const capturedRequests: Array<{ url: string; init: RequestInit }> = [];
        const encoder = new TextEncoder();
        global.fetch = jest
          .fn()
          .mockImplementation((url: string, init: RequestInit) => {
            capturedRequests.push({ url, init });
            const bytes = encoder.encode('{"ok":true}');
            const stream = new ReadableStream({
              start(c) {
                c.enqueue(bytes);
                c.close();
              },
            });
            return Promise.resolve({
              ok: true,
              status: 200,
              statusText: 'OK',
              headers: { has: () => false, get: () => null, forEach: () => {} },
              body: stream,
            });
          });

        const result = await runner.run({
          code: `
            const res = await planfi.call({ method: 'GET', path: '/api/test' });
            return { ok: res.ok, status: res.status };
          `,
          env: {}, // NO PLANFI_KEY in job env
          planfi: { baseUrl: 'https://api.planfi.com' },
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        expect(result.output).toMatchObject({ ok: true, status: 200 });

        // Verify the system key was used
        expect(capturedRequests).toHaveLength(1);
        const sentHeaders = capturedRequests[0].init.headers as Record<
          string,
          string
        >;
        expect(sentHeaders['X-Planfi-Service-Key']).toBe('sys_key_123');
      });

      it('prefers job PLANFI_KEY over system key', async () => {
        // Arrange: both system key and job key are set
        process.env = {
          ...originalEnv,
          SCHEDULED_JOBS_INTERNAL_PLANFI_KEY: 'sys_key_fallback',
        };

        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        const capturedRequests: Array<{ url: string; init: RequestInit }> = [];
        const encoder = new TextEncoder();
        global.fetch = jest
          .fn()
          .mockImplementation((url: string, init: RequestInit) => {
            capturedRequests.push({ url, init });
            const bytes = encoder.encode('{"ok":true}');
            const stream = new ReadableStream({
              start(c) {
                c.enqueue(bytes);
                c.close();
              },
            });
            return Promise.resolve({
              ok: true,
              status: 200,
              statusText: 'OK',
              headers: { has: () => false, get: () => null, forEach: () => {} },
              body: stream,
            });
          });

        const result = await runner.run({
          code: `
            const res = await planfi.call({ method: 'GET', path: '/api/test' });
            return { ok: res.ok, status: res.status };
          `,
          env: { PLANFI_KEY: 'job_specific_key' },
          planfi: { baseUrl: 'https://api.planfi.com' },
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');

        // Verify the job key was preferred over system key
        expect(capturedRequests).toHaveLength(1);
        const sentHeaders = capturedRequests[0].init.headers as Record<
          string,
          string
        >;
        expect(sentHeaders['X-Planfi-Service-Key']).toBe('job_specific_key');
      });

      it('rejects with clear error when neither job nor system key is available', async () => {
        // Arrange: no system key set
        process.env = { ...originalEnv };
        delete process.env.SCHEDULED_JOBS_INTERNAL_PLANFI_KEY;

        jest
          .spyOn(dns, 'lookup')
          // @ts-expect-error overload {all:true} returns array
          .mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);

        const result = await runner.run({
          code: `
            try {
              await planfi.call({ path: '/api/test' });
              return 'NO_THROW';
            } catch (e) {
              return { caught: true, msg: String(e.message || e) };
            }
          `,
          env: {}, // no PLANFI_KEY
          planfi: { baseUrl: 'https://api.planfi.com' },
          limits: { timeoutMs: 3000, memoryMb: 64 },
        });

        expect(result.status).toBe('SUCCESS');
        expect(result.output).toMatchObject({ caught: true });
        const msg = (result.output as { msg: string }).msg;
        // Must mention both sources
        expect(msg).toContain('SCHEDULED_JOBS_INTERNAL_PLANFI_KEY');
        expect(msg).toContain('PLANFI_KEY');
      });
    });
  });

  // -------------------------------------------------------------------------
  // dry-run modo seguro (writeMode safe)
  // -------------------------------------------------------------------------

  describe('dry-run modo seguro (writeMode safe)', () => {
    const realFetch = global.fetch;
    afterEach(() => {
      global.fetch = realFetch;
    });

    it('não dispara fetch em método de escrita e devolve resposta simulada', async () => {
      global.fetch = jest.fn();

      const runner = new SandboxRunner();
      const result = await runner.run({
        code: `
        const r = await planfi.call({ method: 'POST', path: '/api/clients', body: { name: 'x' } });
        return r;
      `,
        env: { PLANFI_KEY: 'pf_sk_testkey' },
        limits: { timeoutMs: 3000, memoryMb: 64 },
        planfi: { baseUrl: 'https://planfi.example.com', writeMode: 'safe' },
      });

      expect(result.status).toBe('SUCCESS');
      const out = result.output as {
        ok: boolean;
        statusText: string;
        body: string;
      };
      expect(out.ok).toBe(true);
      expect(out.statusText).toBe('OK (simulado)');
      expect(JSON.parse(out.body).__simulated).toBe(true);
      expect(global.fetch).not.toHaveBeenCalled();
    });
  });
});
