import { describe, it, expect, afterEach } from 'vitest';
import type { Server } from 'http';
import * as http from 'http';
import { buildRedirectLocation, createHttpRedirectServer } from './http-redirect.js';

describe('buildRedirectLocation', () => {
  it('pins the host and preserves path + query', () => {
    expect(buildRedirectLocation('app.lan', 9876, '/tasks?id=5')).toBe('https://app.lan:9876/tasks?id=5');
  });

  it('buildRedirectLocation never echoes a caller-supplied host string', () => {
    // The function takes pinnedHost as a parameter — it cannot echo an attacker host
    // because the host is always taken from the first argument, not from reqUrl.
    // Pass a reqUrl that looks like it contains a host to confirm it is ignored.
    const loc = buildRedirectLocation('app.lan', 9876, '/');
    expect(loc).toBe('https://app.lan:9876/');
    expect(loc).not.toContain('evil.attacker.com');
  });

  it('strips CR/LF/space from the path to prevent header injection', () => {
    const loc = buildRedirectLocation('app.lan', 9876, '/a\r\nSet-Cookie: x=1 b');
    expect(loc).not.toMatch(/[\r\n ]/);
  });

  it('omits the port for 443', () => {
    expect(buildRedirectLocation('app.lan', 443, '/')).toBe('https://app.lan/');
  });

  it('prefixes a leading slash when the url lacks one', () => {
    expect(buildRedirectLocation('app.lan', 9876, 'x')).toBe('https://app.lan:9876/x');
  });
});

describe('createHttpRedirectServer', () => {
  let server: Server;
  afterEach(() => new Promise<void>((r) => (server ? server.close(() => r()) : r())));

  it('responds 301 with the pinned https Location', async () => {
    server = createHttpRedirectServer({ httpsPort: 9876, pinnedHost: 'app.lan' });
    await new Promise<void>((r) => server.listen(0, '127.0.0.1', r));
    const addr = server.address();
    const port = typeof addr === 'object' && addr ? addr.port : 0;
    const res = await fetch(`http://127.0.0.1:${port}/x?y=1`, { redirect: 'manual' });
    expect(res.status).toBe(301);
    expect(res.headers.get('location')).toBe('https://app.lan:9876/x?y=1');
  });

  it('ignores an attacker-controlled Host header (open-redirect guard)', async () => {
    const srv = createHttpRedirectServer({ httpsPort: 9876, pinnedHost: 'app.lan' });
    await new Promise<void>((r) => srv.listen(0, '127.0.0.1', r));
    try {
      const addr = srv.address();
      const port = typeof addr === 'object' && addr ? addr.port : 0;
      // Use node's http.request so we can set the Host header explicitly.
      // (Some fetch implementations silently drop Host overrides.)
      const loc = await new Promise<string>((resolve, reject) => {
        const req = http.request(
          { hostname: '127.0.0.1', port, path: '/x', method: 'GET', headers: { Host: 'evil.attacker.com' } },
          (res) => resolve(res.headers['location'] ?? ''),
        );
        req.on('error', reject);
        req.end();
      });
      expect(loc).toBe('https://app.lan:9876/x');
      expect(loc).not.toContain('evil.attacker.com');
    } finally {
      await new Promise<void>((r) => srv.close(() => r()));
    }
  });
});
