import { describe, it, expect } from 'vitest';
import { getClientIp, isTrustedProxyIp } from './rateLimit.js';

// Minimal Hono-Context stand-in: headers + the node-server env with the
// socket peer address.
function fakeContext(opts: { peer?: string; headers?: Record<string, string> }) {
  const headers = new Map(
    Object.entries(opts.headers || {}).map(([k, v]) => [k.toLowerCase(), v])
  );
  return {
    req: { header: (name: string) => headers.get(name.toLowerCase()) },
    env: opts.peer ? { incoming: { socket: { remoteAddress: opts.peer } } } : {},
  } as any;
}

describe('isTrustedProxyIp', () => {
  it('trusts loopback and private ranges, including IPv4-mapped IPv6', () => {
    expect(isTrustedProxyIp('127.0.0.1')).toBe(true);
    expect(isTrustedProxyIp('::1')).toBe(true);
    expect(isTrustedProxyIp('::ffff:127.0.0.1')).toBe(true);
    expect(isTrustedProxyIp('10.1.2.3')).toBe(true);
    expect(isTrustedProxyIp('172.18.0.5')).toBe(true);
    expect(isTrustedProxyIp('192.168.1.1')).toBe(true);
  });

  it('does not trust public addresses or near-miss ranges', () => {
    expect(isTrustedProxyIp('203.0.113.7')).toBe(false);
    expect(isTrustedProxyIp('172.15.0.1')).toBe(false); // outside 172.16/12
    expect(isTrustedProxyIp('172.32.0.1')).toBe(false);
    expect(isTrustedProxyIp('1270.0.0.1')).toBe(false);
    expect(isTrustedProxyIp('')).toBe(false);
  });
});

describe('getClientIp', () => {
  it('prefers X-Real-IP when the peer is a trusted proxy', () => {
    const c = fakeContext({
      peer: '127.0.0.1',
      headers: { 'x-real-ip': '203.0.113.7', 'x-forwarded-for': '9.9.9.9' },
    });
    expect(getClientIp(c)).toBe('203.0.113.7');
  });

  it('walks X-Forwarded-For from the right past our own proxy hops', () => {
    // spoofed prefix, then the real client appended by nginx, then the Next
    // proxy hop — the rightmost untrusted entry wins
    const c = fakeContext({
      peer: '127.0.0.1',
      headers: { 'x-forwarded-for': '9.9.9.9, 203.0.113.7, 127.0.0.1' },
    });
    expect(getClientIp(c)).toBe('203.0.113.7');
  });

  it('ignores forwarded headers entirely when the peer is untrusted', () => {
    // A client hitting the API directly cannot pick its own bucket
    const c = fakeContext({
      peer: '198.51.100.4',
      headers: { 'x-forwarded-for': '9.9.9.9', 'x-real-ip': '8.8.8.8' },
    });
    expect(getClientIp(c)).toBe('198.51.100.4');
  });

  it('falls back to the socket address for local traffic with no headers', () => {
    expect(getClientIp(fakeContext({ peer: '127.0.0.1' }))).toBe('127.0.0.1');
    expect(getClientIp(fakeContext({ peer: '::ffff:127.0.0.1' }))).toBe('127.0.0.1');
  });

  it('falls back to the socket address when every forwarded hop is internal', () => {
    const c = fakeContext({
      peer: '127.0.0.1',
      headers: { 'x-forwarded-for': '127.0.0.1' },
    });
    expect(getClientIp(c)).toBe('127.0.0.1');
  });

  it('rejects junk header values instead of using them as bucket keys', () => {
    const c = fakeContext({
      peer: '127.0.0.1',
      headers: { 'x-forwarded-for': 'not-an-ip; DROP TABLE users' },
    });
    expect(getClientIp(c)).toBe('127.0.0.1');
  });

  it('returns "unknown" without a socket address or trusted headers', () => {
    expect(getClientIp(fakeContext({}))).toBe('unknown');
  });
});
