{{ header_comment }}
package {{ java_group_id }}.e2e;

import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;

/**
 * Lazily spawns a mock-server for e2e tests, unless MOCK_SERVER_URL is pre-set
 * by the parent (e.g., 'alef test-apps run'). Exposes the chosen URL and
 * per-fixture endpoints as mockServerUrl and mockServer.<fixture_id> system
 * properties respectively. Respects the e2e/rust/src/main.rs mock-server contract:
 * reads MOCK_SERVER_URL= and MOCK_SERVERS= from stdout, validates TCP readiness,
 * and spawns per-fixture mock listeners in isolated threads (fallback) so tests can run via plain `mvn test` without any external
 * mock-server orchestration. Mirrors the Ruby spec_helper / Python
 * conftest spawn pattern. Honors a pre-set MOCK_SERVER_URL by
 * skipping the spawn entirely.
 */
public class MockServerListener implements LauncherSessionListener {
    private Process mockServer;

    @Override
    public void launcherSessionOpened(LauncherSession session) {
        String preset = System.getenv("MOCK_SERVER_URL");
        if (preset != null && !preset.isEmpty()) {
            System.setProperty("mockServerUrl", preset);
            // Even when MOCK_SERVER_URL is preset, parse MOCK_SERVERS for per-fixture endpoints.
            String mockServersEnv = System.getenv("MOCK_SERVERS");
            if (mockServersEnv != null && !mockServersEnv.isEmpty()) {
                System.setProperty("mockServers", mockServersEnv);
                // Parse JSON map of fixture_id -> url and expose as system properties.
                Pattern p = Pattern.compile("\"([^\"]+)\":\"([^\"]+)\"");
                Matcher matcher = p.matcher(mockServersEnv);
                while (matcher.find()) {
                    String fid = matcher.group(1);
                    String furl = matcher.group(2);
                    System.setProperty("mockServer." + fid, furl);
                }
            }
            return;
        }
        Path repoRoot = locateRepoRoot();
        if (repoRoot == null) {
            throw new IllegalStateException("MockServerListener: could not locate repo root (looked for fixtures/ in ancestors of " + System.getProperty("user.dir") + ")");
        }
        String binName = System.getProperty("os.name", "").toLowerCase().contains("win") ? "mock-server.exe" : "mock-server";
        File bin = repoRoot.resolve("e2e").resolve("rust").resolve("target").resolve("release").resolve(binName).toFile();
        File fixturesDir = repoRoot.resolve("fixtures").toFile();
        if (!bin.exists()) {
            throw new IllegalStateException("MockServerListener: mock-server binary not found at " + bin + " — run: cargo build --manifest-path e2e/rust/Cargo.toml --bin mock-server --release");
        }
        ProcessBuilder pb = new ProcessBuilder(bin.getAbsolutePath(), fixturesDir.getAbsolutePath())
            .redirectErrorStream(false);
        try {
            mockServer = pb.start();
        } catch (IOException e) {
            throw new IllegalStateException("MockServerListener: failed to start mock-server", e);
        }
        // Read until we see MOCK_SERVER_URL= and optionally MOCK_SERVERS=.
        // Cap the loop so a misbehaving mock-server cannot block indefinitely.
        BufferedReader stdout = new BufferedReader(new InputStreamReader(mockServer.getInputStream(), StandardCharsets.UTF_8));
        String url = null;
        try {
            for (int i = 0; i < 16; i++) {
                String line = stdout.readLine();
                if (line == null) break;
                if (line.startsWith("MOCK_SERVER_URL=")) {
                    url = line.substring("MOCK_SERVER_URL=".length()).trim();
                } else if (line.startsWith("MOCK_SERVERS=")) {
                    String jsonVal = line.substring("MOCK_SERVERS=".length()).trim();
                    System.setProperty("mockServers", jsonVal);
                    // Parse JSON map of fixture_id -> url and expose as system properties.
                    Pattern p = Pattern.compile("\"([^\"]+)\":\"([^\"]+)\"");
                    Matcher matcher = p.matcher(jsonVal);
                    while (matcher.find()) {
                        String fid = matcher.group(1);
                        String furl = matcher.group(2);
                        System.setProperty("mockServer." + fid, furl);
                    }
                    break;
                } else if (url != null) {
                    break;
                }
            }
        } catch (IOException e) {
            mockServer.destroyForcibly();
            throw new IllegalStateException("MockServerListener: failed to read mock-server stdout", e);
        }
        if (url == null || url.isEmpty()) {
            mockServer.destroyForcibly();
            throw new IllegalStateException("MockServerListener: mock-server did not emit MOCK_SERVER_URL");
        }
        // TCP-readiness probe: ensure axum::serve is accepting before tests start.
        // The mock-server binds the TcpListener synchronously then prints the URL
        // before tokio::spawn(axum::serve(...)) is polled, so under Surefire
        // parallel mode tests can race startup. Poll-connect (max 5s, 50ms backoff)
        // until success.
        java.net.URI healthUri = java.net.URI.create(url);
        String host = healthUri.getHost();
        int port = healthUri.getPort();
        long deadline = System.nanoTime() + 5_000_000_000L;
        while (System.nanoTime() < deadline) {
            try (java.net.Socket s = new java.net.Socket()) {
                s.connect(new java.net.InetSocketAddress(host, port), 100);
                break;
            } catch (java.io.IOException ignored) {
                try { Thread.sleep(50); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; }
            }
        }
        System.setProperty("mockServerUrl", url);
        // Drain remaining stdout/stderr in daemon threads so a full pipe
        // does not block the child.
        Process server = mockServer;
        Thread drainOut = new Thread(() -> drain(stdout));
        drainOut.setDaemon(true);
        drainOut.start();
        Thread drainErr = new Thread(() -> drain(new BufferedReader(new InputStreamReader(server.getErrorStream(), StandardCharsets.UTF_8))));
        drainErr.setDaemon(true);
        drainErr.start();
    }

    @Override
    public void launcherSessionClosed(LauncherSession session) {
        if (mockServer == null) return;
        try { mockServer.getOutputStream().close(); } catch (IOException ignored) {}
        try {
            if (!mockServer.waitFor(2, java.util.concurrent.TimeUnit.SECONDS)) {
                mockServer.destroyForcibly();
            }
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            mockServer.destroyForcibly();
        }
    }

    private static Path locateRepoRoot() {
        Path dir = Paths.get("").toAbsolutePath();
        while (dir != null) {
            if (dir.resolve("fixtures").toFile().isDirectory()
                && dir.resolve("e2e").toFile().isDirectory()) {
                return dir;
            }
            dir = dir.getParent();
        }
        return null;
    }

    private static void drain(BufferedReader reader) {
        try {
            char[] buf = new char[1024];
            while (reader.read(buf) >= 0) { /* drain */ }
        } catch (IOException ignored) {}
    }
}
