import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { chmod, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = fileURLToPath(new URL('..', import.meta.url));
const hermesFixture = fileURLToPath(new URL('./fixtures/fake-hermes.mjs', import.meta.url));
let nextPort = 43100;

async function startServer(t, env = {}) {
  const port = nextPort++;
  const origin = `http://127.0.0.1:${port}`;
  const child = spawn(process.execPath, ['server.mjs'], {
    cwd: root,
    env: { ...process.env, PORT: String(port), ...env, ...(env.TIMMY_PUBLIC_ORIGIN === '__ORIGIN__' ? { TIMMY_PUBLIC_ORIGIN: origin } : {}) },
    stdio: ['ignore', 'pipe', 'pipe'],
  });
  let stderr = '';
  let stdout = '';
  child.stdout.on('data', chunk => { stdout += chunk; });
  child.stderr.on('data', chunk => { stderr += chunk; });
  t.after(() => child.kill('SIGTERM'));
  const deadline = Date.now() + 10_000;
  const basePath = (env.TIMMY_BASE_PATH || '').replace(/\/$/, '');
  while (Date.now() < deadline) {
    if (child.exitCode !== null) throw new Error(`server exited ${child.exitCode}: ${stderr}`);
    try {
      const response = await fetch(`${origin}${basePath}/api/healthz`);
      if (response.status) return { origin, child, getStdout: () => stdout };
    } catch {}
    await new Promise(resolve => setTimeout(resolve, 40));
  }
  throw new Error(`server did not become ready: ${stderr}`);
}

test('staging host can bind loopback instead of every network interface', async t => {
  const { getStdout } = await startServer(t, { HOST: '127.0.0.1', TIMMY_VISION_ENABLED: '0' });
  assert.match(getStdout(), /http:\/\/127\.0\.0\.1:/);
  assert.doesNotMatch(getStdout(), /http:\/\/0\.0\.0\.0:/);
});

test('health endpoint exposes only bounded staging identity and feature flags', async t => {
  const { origin } = await startServer(t, {
    TIMMY_RELEASE_TAG: 'daily-2026-08-20.3',
    TIMMY_RELEASE_COMMIT: 'ca31e6d38bec649407f63880504554c59f2878ae',
    TIMMY_VISION_ENABLED: '0',
    TIMMY_AGENT_ENABLED: 'false',
    SECRET_TOKEN: 'must-not-leak',
  });

  const response = await fetch(`${origin}/api/healthz`);
  assert.equal(response.status, 200);
  assert.deepEqual(await response.json(), {
    ok: true,
    release: 'daily-2026-08-20.3',
    commit: 'ca31e6d38bec649407f63880504554c59f2878ae',
    visionEnabled: false,
    agentEnabled: false,
  });
  assert.deepEqual([...response.headers.keys()].filter(name => /token|cookie|session|path|environment|credential/i.test(name)), []);
});

test('base path contains static files and APIs without capturing sibling routes', async t => {
  const { origin } = await startServer(t, { TIMMY_BASE_PATH: '/timmy-staging', TIMMY_VISION_ENABLED: '0' });

  const health = await fetch(`${origin}/timmy-staging/api/healthz`);
  assert.equal(health.status, 200);
  const page = await fetch(`${origin}/timmy-staging/`);
  assert.equal(page.status, 200);
  assert.match(await page.text(), /<div id="app"/);
  assert.equal((await fetch(`${origin}/timmy-staging/app.js`)).status, 200);
  assert.equal((await fetch(`${origin}/api/healthz`)).status, 404);
  assert.equal((await fetch(`${origin}/git`)).status, 404);
});

test('prefixed document, manifest, and service worker stay inside the app scope', async t => {
  const { origin } = await startServer(t, {
    TIMMY_BASE_PATH: '/timmy-staging',
    TIMMY_RELEASE_TAG: 'daily-2026-08-20.3',
    TIMMY_RELEASE_COMMIT: 'ca31e6d38bec649407f63880504554c59f2878ae',
    TIMMY_STAGING_LABEL: 'true',
    TIMMY_VISION_ENABLED: '0',
  });

  const redirect = await fetch(`${origin}/timmy-staging`, { redirect: 'manual' });
  assert.equal(redirect.status, 308);
  assert.equal(redirect.headers.get('location'), '/timmy-staging/');

  const html = await (await fetch(`${origin}/timmy-staging/`)).text();
  assert.match(html, /<base href="\/timmy-staging\/">/);
  assert.doesNotMatch(html, /window\.__TIMMY_CONFIG__\s*=/);
  assert.match(html, /<meta name="timmy-base-path" content="\/timmy-staging">/);
  assert.match(html, /<meta name="timmy-staging-label" content="Staging · daily-2026-08-20\.3 · ca31e6d38bec">/);

  const manifest = await (await fetch(`${origin}/timmy-staging/manifest.webmanifest`)).json();
  assert.equal(manifest.start_url, '/timmy-staging/');
  assert.equal(manifest.scope, '/timmy-staging/');
  assert.ok(manifest.icons.every(icon => icon.src.startsWith('/timmy-staging/')));
  assert.equal((await fetch(`${origin}/timmy-staging/service-worker.js`)).status, 200);
});

test('agent cookie is constrained to the normalized base path', async t => {
  const workdir = await mkdtemp(join(tmpdir(), 'timmy-staging-cookie-'));
  await chmod(hermesFixture, 0o700);
  t.after(() => rm(workdir, { recursive: true, force: true }));
  const { origin } = await startServer(t, {
    TIMMY_BASE_PATH: '/timmy-staging/',
    TIMMY_AGENT_ENABLED: 'true',
    TIMMY_AGENT_ACCESS_TOKEN: 'test-integration-access-code-2026',
    TIMMY_PUBLIC_ORIGIN: '__ORIGIN__',
    TIMMY_AGENT_WORKDIR: workdir,
    TIMMY_HERMES_COMMAND: hermesFixture,
    TIMMY_VISION_ENABLED: '0',
  });

  const response = await fetch(`${origin}/timmy-staging/api/agent/unlock`, {
    method: 'POST',
    headers: { 'content-type': 'application/json', origin, 'sec-fetch-site': 'same-origin' },
    body: JSON.stringify({ accessCode: 'test-integration-access-code-2026' }),
  });
  assert.equal(response.status, 200);
  assert.match(response.headers.get('set-cookie'), /; Path=\/timmy-staging\//);
});

test('malformed and escaping base paths are rejected at startup', async () => {
  const invalid = [
    'timmy-staging',
    '/../git',
    '/timmy/./nested',
    '/%2e%2e/git',
    '/%252e%252e/git',
    '/timmy%2Fgit',
    '/timmy%5cgit',
    '/timmy%2dstaging',
    '/timmy\\git',
    '/timmy?debug=1',
    '/timmy#fragment',
    '/timmy%',
    '/timmy//nested',
    '/timmy-staging//',
    '/timmy-staging;Secure',
    '/timmy-staging"quoted',
    "/timmy-staging'quoted",
    '/timmy staging',
    '/timmy\nstaging',
  ];

  for (const value of invalid) {
    const child = spawn(process.execPath, ['server.mjs'], {
      cwd: root,
      env: { ...process.env, PORT: '0', TIMMY_BASE_PATH: value },
      stdio: ['ignore', 'ignore', 'pipe'],
    });
    let stderr = '';
    child.stderr.on('data', chunk => { stderr += chunk; });
    const exitCode = await Promise.race([
      new Promise(resolve => child.once('exit', resolve)),
      new Promise(resolve => setTimeout(() => { child.kill('SIGTERM'); resolve('timeout'); }, 800)),
    ]);
    assert.notEqual(exitCode, 'timeout', `${JSON.stringify(value)} was accepted`);
    assert.notEqual(exitCode, 0, `${JSON.stringify(value)} exited successfully`);
    assert.match(stderr, /TIMMY_BASE_PATH/);
  }
});
