#!/usr/bin/env node
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// src/report-server.ts
var report_server_exports = {};
__export(report_server_exports, {
  startReportServer: () => startReportServer
});
function sendJson(res, status, body) {
  res.writeHead(status, {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*"
  });
  res.end(JSON.stringify(body));
}
function startReportServer(reportDir, options) {
  return new Promise((resolve2, reject) => {
    const startPort = options?.port ?? DEFAULT_PORT;
    let currentAttempt = 0;
    let inactivityTimer;
    function resetTimer() {
      clearTimeout(inactivityTimer);
      inactivityTimer = setTimeout(() => {
        server.close();
      }, INACTIVITY_TIMEOUT_MS);
    }
    const server = (0, import_node_http.createServer)((req, res) => {
      resetTimer();
      res.setHeader("Access-Control-Allow-Origin", "*");
      res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
      if (req.method === "OPTIONS") {
        res.writeHead(204);
        res.end();
        return;
      }
      const pathname = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`).pathname;
      if (req.method === "GET" && (pathname === "/" || pathname === "/report.html")) {
        const htmlPath = (0, import_node_path2.join)(reportDir, "report.html");
        if ((0, import_node_fs2.existsSync)(htmlPath)) {
          res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
          res.end((0, import_node_fs2.readFileSync)(htmlPath));
        } else {
          sendJson(res, 404, { error: "report.html not found. Run your tests first." });
        }
        return;
      }
      if (req.method === "GET" && pathname === "/api/health") {
        sendJson(res, 200, { status: "ok", reportMode: "streaming" });
        return;
      }
      if (req.method === "GET" && pathname === "/api/summary") {
        const summaryPath = (0, import_node_path2.join)(reportDir, "summary.json");
        if ((0, import_node_fs2.existsSync)(summaryPath)) {
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end((0, import_node_fs2.readFileSync)(summaryPath, "utf-8"));
        } else {
          sendJson(res, 404, { error: "Summary not found" });
        }
        return;
      }
      if (req.method === "GET" && pathname === "/api/tests") {
        const indexPath = (0, import_node_path2.join)(reportDir, "index.json");
        if ((0, import_node_fs2.existsSync)(indexPath)) {
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end((0, import_node_fs2.readFileSync)(indexPath, "utf-8"));
        } else {
          sendJson(res, 404, { error: "Index not found" });
        }
        return;
      }
      const testMatch = pathname.match(/^\/api\/tests\/([a-f0-9]+)$/);
      if (req.method === "GET" && testMatch) {
        const metaPath = (0, import_node_path2.join)(reportDir, "tests", testMatch[1], "meta.json");
        if ((0, import_node_fs2.existsSync)(metaPath)) {
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end((0, import_node_fs2.readFileSync)(metaPath, "utf-8"));
        } else {
          sendJson(res, 404, { error: "Test not found" });
        }
        return;
      }
      if (req.method === "GET" && pathname.startsWith("/artifacts/")) {
        const relPath = decodeURIComponent(pathname.slice("/artifacts/".length));
        if (relPath.includes("..") || relPath.includes("\0")) {
          sendJson(res, 400, { error: "Invalid path" });
          return;
        }
        const filePath = (0, import_node_path2.join)(reportDir, "artifacts", relPath);
        if ((0, import_node_fs2.existsSync)(filePath)) {
          const ext = (0, import_node_path2.extname)(filePath).toLowerCase();
          res.writeHead(200, { "Content-Type": MIME_TYPES[ext] ?? "application/octet-stream" });
          res.end((0, import_node_fs2.readFileSync)(filePath));
        } else {
          sendJson(res, 404, { error: "File not found" });
        }
        return;
      }
      if (req.method === "POST" && pathname === "/api/shutdown") {
        sendJson(res, 200, { status: "shutting_down" });
        server.close();
        return;
      }
      sendJson(res, 404, { error: "Not found" });
    });
    function tryListen(port) {
      server.once("error", (err) => {
        if (err.code === "EADDRINUSE" && currentAttempt < MAX_PORT_ATTEMPTS) {
          currentAttempt++;
          tryListen(port + 1);
        } else {
          reject(err);
        }
      });
      server.listen(port, "127.0.0.1", () => {
        const addr = server.address();
        if (!addr || typeof addr === "string") {
          reject(new Error("Failed to get server address"));
          return;
        }
        resetTimer();
        resolve2({
          port: addr.port,
          dispose: () => new Promise((res) => {
            clearTimeout(inactivityTimer);
            server.close(() => res());
          })
        });
      });
    }
    tryListen(startPort);
  });
}
var import_node_http, import_node_path2, import_node_fs2, DEFAULT_PORT, MAX_PORT_ATTEMPTS, INACTIVITY_TIMEOUT_MS, MIME_TYPES;
var init_report_server = __esm({
  "src/report-server.ts"() {
    "use strict";
    import_node_http = require("http");
    import_node_path2 = require("path");
    import_node_fs2 = require("fs");
    DEFAULT_PORT = 9323;
    MAX_PORT_ATTEMPTS = 10;
    INACTIVITY_TIMEOUT_MS = 30 * 60 * 1e3;
    MIME_TYPES = {
      ".png": "image/png",
      ".jpg": "image/jpeg",
      ".jpeg": "image/jpeg",
      ".gif": "image/gif",
      ".webp": "image/webp",
      ".webm": "video/webm",
      ".mp4": "video/mp4",
      ".json": "application/json",
      ".html": "text/html",
      ".css": "text/css",
      ".js": "text/javascript",
      ".svg": "image/svg+xml"
    };
  }
});

// src/browser-open.ts
var browser_open_exports = {};
__export(browser_open_exports, {
  openInBrowser: () => openInBrowser
});
function openInBrowser(filePath) {
  try {
    const platform = process.platform;
    let command;
    if (platform === "darwin") {
      command = `open "${filePath}"`;
    } else if (platform === "win32") {
      command = `start "" "${filePath}"`;
    } else {
      command = `xdg-open "${filePath}"`;
    }
    (0, import_node_child_process.exec)(command, (err) => {
      if (err) {
        process.stderr.write(
          `[testrelic] Failed to open browser: ${err.message}
`
        );
      }
    });
  } catch {
  }
}
var import_node_child_process;
var init_browser_open = __esm({
  "src/browser-open.ts"() {
    "use strict";
    import_node_child_process = require("child_process");
  }
});

// src/merge.ts
var import_node_crypto = require("crypto");
var import_node_fs = require("fs");
var import_node_path = require("path");
function isValidReport(obj) {
  if (typeof obj !== "object" || obj === null) return false;
  const r = obj;
  return typeof r.schemaVersion === "string" && typeof r.testRunId === "string" && typeof r.startedAt === "string" && typeof r.completedAt === "string" && typeof r.totalDuration === "number" && typeof r.summary === "object" && Array.isArray(r.timeline);
}
async function mergeReports(files, options) {
  const reports = [];
  for (const file of files) {
    let raw;
    try {
      raw = (0, import_node_fs.readFileSync)(file, "utf-8");
    } catch (err) {
      throw new Error(`Failed to read file: ${file}: ${err instanceof Error ? err.message : String(err)}`);
    }
    let parsed;
    try {
      parsed = JSON.parse(raw);
    } catch (err) {
      throw new Error(`Invalid JSON in file: ${file}: ${err instanceof Error ? err.message : String(err)}`);
    }
    if (!isValidReport(parsed)) {
      throw new Error(`Invalid report schema in file: ${file}`);
    }
    reports.push(parsed);
  }
  if (reports.length === 0) {
    throw new Error("No valid reports to merge");
  }
  const shardRunIds = reports.map((r) => r.testRunId);
  const allTimelines = [];
  for (const report of reports) {
    allTimelines.push(...report.timeline);
  }
  allTimelines.sort((a, b) => {
    const timeA = "visitedAt" in a ? a.visitedAt : a.timestamp;
    const timeB = "visitedAt" in b ? b.visitedAt : b.timestamp;
    return new Date(timeA).getTime() - new Date(timeB).getTime();
  });
  const summary = recalculateSummary(reports.map((r) => r.summary), allTimelines.length);
  const startedAt = reports.reduce(
    (earliest, r) => r.startedAt < earliest ? r.startedAt : earliest,
    reports[0].startedAt
  );
  const completedAt = reports.reduce(
    (latest, r) => r.completedAt > latest ? r.completedAt : latest,
    reports[0].completedAt
  );
  const totalDuration = new Date(completedAt).getTime() - new Date(startedAt).getTime();
  const merged = {
    schemaVersion: reports[0].schemaVersion,
    testRunId: options.testRunId ?? (0, import_node_crypto.randomUUID)(),
    startedAt,
    completedAt,
    totalDuration,
    summary,
    ci: reports.find((r) => r.ci !== null)?.ci ?? null,
    metadata: reports.find((r) => r.metadata !== null)?.metadata ?? null,
    timeline: allTimelines,
    shardRunIds
  };
  const dir = (0, import_node_path.dirname)(options.output);
  (0, import_node_fs.mkdirSync)(dir, { recursive: true });
  (0, import_node_fs.writeFileSync)(options.output, JSON.stringify(merged, null, 2), "utf-8");
  return merged;
}
function recalculateSummary(summaries, timelineLength) {
  let total = 0, passed = 0, failed = 0, flaky = 0, skipped = 0, timedout = 0;
  let totalApiCalls = 0, totalAssertions = 0, passedAssertions = 0, failedAssertions = 0;
  let totalNavigations = 0, totalActionSteps = 0;
  for (const s of summaries) {
    total += s.total;
    passed += s.passed;
    failed += s.failed;
    flaky += s.flaky;
    skipped += s.skipped;
    timedout += s.timedout;
    totalApiCalls += s.totalApiCalls;
    totalAssertions += s.totalAssertions;
    passedAssertions += s.passedAssertions;
    failedAssertions += s.failedAssertions;
    totalNavigations += s.totalNavigations;
    totalActionSteps += s.totalActionSteps;
  }
  return {
    total,
    passed,
    failed,
    flaky,
    skipped,
    timedout,
    totalApiCalls,
    uniqueApiUrls: 0,
    apiCallsByMethod: {},
    apiCallsByStatusRange: { "2xx": 0, "3xx": 0, "4xx": 0, "5xx": 0, error: 0 },
    apiResponseTime: null,
    totalAssertions,
    passedAssertions,
    failedAssertions,
    totalNavigations,
    uniqueNavigationUrls: 0,
    totalTimelineSteps: timelineLength,
    totalActionSteps,
    actionStepsByCategory: {}
  };
}

// src/cli.ts
var import_node_path3 = require("path");
var import_node_fs3 = require("fs");
async function main() {
  const args = process.argv.slice(2);
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
    printUsage();
    process.exit(0);
  }
  const command = args[0];
  if (command === "merge") {
    await handleMerge(args.slice(1));
  } else if (command === "serve" || command === "open") {
    await handleServe(args.slice(1));
  } else {
    process.stderr.write(`Unknown command: ${command}
`);
    printUsage();
    process.exit(1);
  }
}
async function handleMerge(mergeArgs) {
  const outputIndex = mergeArgs.findIndex((a) => a === "-o" || a === "--output");
  if (outputIndex === -1 || outputIndex === mergeArgs.length - 1) {
    process.stderr.write("Error: -o <output> is required\n");
    printUsage();
    process.exit(1);
  }
  const output = mergeArgs[outputIndex + 1];
  const files = [
    ...mergeArgs.slice(0, outputIndex),
    ...mergeArgs.slice(outputIndex + 2)
  ];
  if (files.length === 0) {
    process.stderr.write("Error: No input files specified\n");
    printUsage();
    process.exit(1);
  }
  try {
    const merged = await mergeReports(files, { output });
    process.stderr.write(
      `Merged ${files.length} reports (${merged.summary.total} tests) -> ${output}
`
    );
  } catch (err) {
    process.stderr.write(
      `Error: ${err instanceof Error ? err.message : String(err)}
`
    );
    process.exit(1);
  }
}
async function handleServe(serveArgs) {
  const portIndex = serveArgs.findIndex((a) => a === "--port" || a === "-p");
  let port;
  let dir;
  if (portIndex !== -1 && portIndex < serveArgs.length - 1) {
    port = parseInt(serveArgs[portIndex + 1], 10);
    if (isNaN(port) || port < 1 || port > 65535) {
      process.stderr.write("Error: Invalid port number\n");
      process.exit(1);
    }
    const rest = [...serveArgs.slice(0, portIndex), ...serveArgs.slice(portIndex + 2)];
    dir = rest[0];
  } else {
    dir = serveArgs[0];
  }
  if (!dir) {
    process.stderr.write("Error: Report directory is required\n");
    process.stderr.write("Usage: testrelic-appium serve <dir> [--port <port>]\n");
    process.exit(1);
  }
  const reportDir = (0, import_node_path3.resolve)(dir);
  if (!(0, import_node_fs3.existsSync)(reportDir)) {
    process.stderr.write(`Error: Directory not found: ${reportDir}
`);
    process.exit(1);
  }
  try {
    const { startReportServer: startReportServer2 } = await Promise.resolve().then(() => (init_report_server(), report_server_exports));
    const { openInBrowser: openInBrowser2 } = await Promise.resolve().then(() => (init_browser_open(), browser_open_exports));
    const handle = await startReportServer2(reportDir, port ? { port } : void 0);
    const url = `http://127.0.0.1:${handle.port}`;
    process.stderr.write(`
  Report server running at: ${url}
  Press Ctrl+C to stop

`);
    openInBrowser2(url);
    process.on("SIGINT", () => {
      handle.dispose();
      process.exit(0);
    });
    process.on("SIGTERM", () => {
      handle.dispose();
      process.exit(0);
    });
  } catch (err) {
    process.stderr.write(
      `Error: ${err instanceof Error ? err.message : String(err)}
`
    );
    process.exit(1);
  }
}
function printUsage() {
  process.stderr.write(
    `Usage: testrelic-appium <command>

Commands:
  merge <files...> -o <output>   Merge multiple shard report JSON files
  serve <dir> [--port <port>]    Start a local server for a streaming report
  open <dir> [--port <port>]     Alias for serve

Examples:
  npx testrelic-appium merge shard-1.json shard-2.json -o merged.json
  npx testrelic-appium serve ./test-results/.testrelic-report
  npx testrelic-appium serve ./test-results/.testrelic-report --port 9400
`
  );
}
main();
