import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import fs from 'node:fs/promises';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const projectRoot = fileURLToPath(new URL('../', import.meta.url));
const socialPreviewPath = '/assets/social-preview-v2.78d008df9860.jpg';

test('serves crawler-safe HEAD responses for the homepage and social preview', { timeout: 15_000 }, async (t) => {
  let port;
  try {
    port = await availablePort();
  } catch (error) {
    if (error.code === 'EPERM') {
      t.skip('loopback listeners are disabled in this sandbox');
      return;
    }
    throw error;
  }

  const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'meme-protocol-head-'));
  const child = spawn(process.execPath, ['server.js'], {
    cwd: projectRoot,
    env: {
      ...process.env,
      ADMIN_TOKEN: 'test-admin-token-000000000000',
      DATA_DIR: dataDir,
      HOST: '127.0.0.1',
      PORT: String(port),
      SEED_DEMO_MEMES: 'false',
      SITE_URL: `http://127.0.0.1:${port}`
    },
    stdio: ['ignore', 'pipe', 'pipe']
  });

  t.after(async () => {
    if (child.exitCode === null && child.signalCode === null) {
      child.kill('SIGTERM');
      await once(child, 'exit');
    }
    await fs.rm(dataDir, { recursive: true, force: true });
  });

  await waitForStart(child);

  const headers = { 'user-agent': 'Twitterbot/1.0' };
  const page = await fetch(`http://127.0.0.1:${port}/`, { method: 'HEAD', headers });
  assert.equal(page.status, 200);
  assert.match(page.headers.get('content-type') || '', /^text\/html/);
  assert.ok(Number(page.headers.get('content-length')) > 0);
  assert.equal((await page.arrayBuffer()).byteLength, 0);

  const image = await fetch(`http://127.0.0.1:${port}${socialPreviewPath}`, { method: 'HEAD', headers });
  assert.equal(image.status, 200);
  assert.equal(image.headers.get('content-type'), 'image/jpeg');
  assert.match(image.headers.get('cache-control') || '', /immutable/);
  assert.ok(Number(image.headers.get('content-length')) < 300_000);
  assert.equal((await image.arrayBuffer()).byteLength, 0);
});

async function availablePort() {
  const server = net.createServer();
  await new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', resolve);
  });
  const address = server.address();
  const port = typeof address === 'object' && address ? address.port : 0;
  await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
  return port;
}

async function waitForStart(child) {
  await new Promise((resolve, reject) => {
    let output = '';
    const timeout = setTimeout(() => finish(new Error(`server did not start:\n${output}`)), 10_000);
    const onData = (chunk) => {
      output += chunk.toString();
      if (output.includes('The Meme Protocol listening')) finish();
    };
    const onExit = (code, signal) => finish(new Error(`server exited before startup (${code ?? signal}):\n${output}`));
    const finish = (error) => {
      clearTimeout(timeout);
      child.stdout.off('data', onData);
      child.stderr.off('data', onData);
      child.off('exit', onExit);
      error ? reject(error) : resolve();
    };

    child.stdout.on('data', onData);
    child.stderr.on('data', onData);
    child.once('exit', onExit);
  });
}
