// This tests that invalid hosts or ports with carriage return or newline characters
// in HTTP request urls are stripped away before being sent to the server.

import * as common from '../common/index.mjs';
import assert from 'node:assert';
import http from 'node:http';
import { once } from 'events';
import { inspect } from 'node:util';
import fixtures from '../common/fixtures.js';
import { Worker } from 'node:worker_threads';

const expectedProxyLogs = new Set();
const proxyWorker = new Worker(fixtures.path('proxy-server-worker.js'));

proxyWorker.on('message', common.mustCall((message) => {
  console.log('Received message from worker:', message.type);
  if (message.type === 'proxy-listening') {
    startTest(message.port).then(common.mustCall());
  } else if (message.type === 'proxy-stopped') {
    assert.deepStrictEqual(new Set(message.logs), expectedProxyLogs);
    // Close the server after the proxy is stopped.
    proxyWorker.terminate();
  }
}, 2));

const requests = new Set();
// Create a server that records the requests it gets.
const server = http.createServer((req, res) => {
  requests.add(`http://localhost:${server.address().port}${req.url}`);
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end(`Response for ${req.url}`);
});
proxyWorker.on('exit', common.mustCall(() => {
  server.close();
}));

async function startTest(proxyPort) {
  // Start a minimal proxy server in a worker, we don't do it in this thread
  // because we'll mutate the global http agent here.
  http.globalAgent = new http.Agent({
    proxyEnv: {
      HTTP_PROXY: `http://localhost:${proxyPort}`,
    },
  });

  server.listen(0);
  await once(server, 'listening');
  server.on('error', common.mustNotCall());
  const port = server.address().port.toString();

  const testCases = [
    { host: 'local\rhost', port: port, path: '/carriage-return-in-host' },
    { host: 'local\nhost', port: port, path: '/newline-in-host' },
    { host: 'local\r\nhost', port: port, path: '/crlf-in-host' },
    { host: 'localhost', port: port.substring(0, 1) + '\r' + port.substring(1), path: '/carriage-return-in-port' },
    { host: 'localhost', port: port.substring(0, 1) + '\n' + port.substring(1), path: '/newline-in-port' },
    { host: 'localhost', port: port.substring(0, 1) + '\r\n' + port.substring(1), path: '/crlf-in-port' },
  ];
  const severHost = `localhost:${server.address().port}`;

  let counter = testCases.length;
  const expectedUrls = new Set();
  for (const testCase of testCases) {
    const url = `http://${testCase.host}:${testCase.port}${testCase.path}`;
    // The invalid characters should all be stripped away before being sent.
    const cleanUrl = url.replaceAll(/\r|\n/g, '');
    expectedUrls.add(cleanUrl);
    expectedProxyLogs.add({
      method: 'GET',
      url: cleanUrl,
      headers: {
        'host': severHost,
        'connection': 'close',
        'proxy-connection': 'close',
      },
    });
    http.request(url, common.mustCall((res) => {
      res.on('error', common.mustNotCall());
      res.setEncoding('utf8');
      res.on('data', () => {});
      res.on('end', common.mustCall(() => {
        console.log(`#${counter--} ended response for: ${inspect(url)}`);
        // Finished all test cases.
        if (counter === 0) {
          // Check that the requests received by the server have sanitized URLs.
          assert.deepStrictEqual(requests, expectedUrls);
          console.log('Sending stop-proxy message to worker');
          proxyWorker.postMessage({ type: 'stop-proxy' });
        }
      }));
    })).on('error', common.mustNotCall()).end();
  }
}
