// server.js — fixture for tests that need a running zddc-server.
//
// Exported `startMaster()` builds the binary if needed (via the canonical
// podman/zddc-go:1.24 invocation from AGENTS.md), seeds a minimal master
// root in a tempdir, spawns the binary on a free port, and resolves once
// it's listening. Returns { baseURL, root, stop() }.
//
// The build step is skipped when ZDDC_TEST_BIN is set in the environment;
// otherwise the binary is cached at <repo>/zddc/dist/zddc-server-test
// with a sibling .hash file holding the SHA256 of the source tree
// (cmd/+internal/+go.{mod,sum}). A second test run reuses the cache when
// the hash is unchanged.
//
// CommonJS form because Playwright's loader handles top-level ESM
// `import` in *.spec.js but doesn't transform the .js helpers we ship
// alongside; mixing leads to "exports is not defined in ES module
// scope" at the helper's first line. Spec files use `import { ... }
// from './lib/server.js'`; the import resolves through CommonJS interop.

const { spawn, execFileSync } = require('child_process');
const fs = require('fs');
const { tmpdir } = require('os');
const { join, resolve, dirname } = require('path');
const { createHash } = require('crypto');
const net = require('net');

const REPO_ROOT = resolve(__dirname, '..', '..');

async function freePort() {
    return await new Promise((res, rej) => {
        const srv = net.createServer();
        srv.unref();
        srv.on('error', rej);
        srv.listen(0, '127.0.0.1', () => {
            const { port } = srv.address();
            srv.close(() => res(port));
        });
    });
}

function sourceHash() {
    const dirs = [join(REPO_ROOT, 'zddc', 'cmd'), join(REPO_ROOT, 'zddc', 'internal')];
    const files = [join(REPO_ROOT, 'zddc', 'go.mod'), join(REPO_ROOT, 'zddc', 'go.sum')];
    const h = createHash('sha256');
    function visit(p) {
        const s = fs.statSync(p);
        if (s.isDirectory()) {
            for (const e of fs.readdirSync(p).sort()) visit(join(p, e));
        } else {
            h.update(p);
            h.update(fs.readFileSync(p));
        }
    }
    for (const d of dirs) if (fs.existsSync(d)) visit(d);
    for (const f of files) if (fs.existsSync(f)) visit(f);
    return h.digest('hex');
}

function ensureBinary() {
    if (process.env.ZDDC_TEST_BIN) return process.env.ZDDC_TEST_BIN;

    const binDir = join(REPO_ROOT, 'zddc', 'dist');
    const binPath = join(binDir, 'zddc-server-test');
    const stampPath = join(binDir, 'zddc-server-test.hash');
    const wantHash = sourceHash();

    if (fs.existsSync(binPath) && fs.existsSync(stampPath)) {
        const have = fs.readFileSync(stampPath, 'utf8').trim();
        if (have === wantHash) return binPath;
    }

    fs.mkdirSync(binDir, { recursive: true });
    const args = [
        'run', '--rm', '--network=host',
        '-v', `${REPO_ROOT}:/src:Z`,
        '-v', `/tmp/gocache:/root/go/pkg/mod:Z`,
        '-v', `${binDir}:/out:Z`,
        '-w', '/src/zddc',
        '-e', 'GOPROXY=https://proxy.golang.org',
        '-e', 'GOSUMDB=off',
        '-e', 'GOPRIVATE=',
        'localhost/zddc-go:1.24',
        'go', 'build', '-o', '/out/zddc-server-test', './cmd/zddc-server',
    ];
    execFileSync('podman', args, { stdio: 'inherit' });
    fs.writeFileSync(stampPath, wantHash);
    return binPath;
}

async function waitListening(url, timeoutMs = 10000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        try {
            const r = await fetch(url, { method: 'GET' });
            if (r.status >= 200 && r.status < 500) return;
        } catch {
            // not listening yet
        }
        await new Promise(r => setTimeout(r, 100));
    }
    throw new Error(`server did not start listening at ${url} within ${timeoutMs}ms`);
}

async function startMaster(opts = {}) {
    const adminEmail = opts.adminEmail || 'alice@example.com';
    const noAuth = opts.noAuth === true;

    const root = fs.mkdtempSync(join(tmpdir(), 'zddc-test-'));
    fs.writeFileSync(join(root, '.zddc'), [
        `title: "Playwright test fixture"`,
        `admins:`,
        `  - ${adminEmail}`,
        `acl:`,
        `  permissions:`,
        `    "${adminEmail}": rwcda`,
        `    "*@example.com": r`,
        '',
    ].join('\n'));

    const bin = ensureBinary();
    const port = await freePort();
    const baseURL = `http://127.0.0.1:${port}`;

    const args = [
        '--root', root,
        '--addr', `127.0.0.1:${port}`,
        '--tls-cert=none',
    ];
    if (noAuth) args.push('--no-auth');

    const proc = spawn(bin, args, {
        stdio: ['ignore', 'pipe', 'pipe'],
        detached: false,
    });

    const buf = [];
    proc.stdout.on('data', d => buf.push(d.toString()));
    proc.stderr.on('data', d => buf.push(d.toString()));

    let exited = false;
    proc.on('exit', () => { exited = true; });

    try {
        await waitListening(`${baseURL}/`, 10000);
    } catch (e) {
        proc.kill('SIGTERM');
        throw new Error(`startMaster failed: ${e.message}\nServer logs:\n${buf.join('')}`);
    }

    return {
        baseURL,
        root,
        proc,
        logs: () => buf.join(''),
        async stop() {
            if (exited) return;
            proc.kill('SIGTERM');
            await new Promise(res => {
                const t = setTimeout(() => { proc.kill('SIGKILL'); res(); }, 3000);
                proc.on('exit', () => { clearTimeout(t); res(); });
            });
        },
    };
}

module.exports = { startMaster };
