/**
 * cli:run-smoke — types.ts
 *
 * Zod contract for the runtime smoke test. Single source of truth for
 * the CLI input shape AND the structured report emitted in the
 * `executeEnvelope.report` field.
 */
import { z } from 'zod';

// ─── Input ──────────────────────────────────────────────────────────────

export const RunSmokeArgsSchema = z.object({
  /** Absolute or relative path to the freshly-generated feature folder
   *  (contains both `web/<app>-web` and the .NET solution under
   *  `src/`). The CLI auto-detects sub-paths when this points to the
   *  feature root. */
  projectPath: z.string().min(1),

  /** Path to the frontend PRD slice (the canonical source of expected
   *  pages + endpoints). Required — the smoke test does not invent
   *  routes ; it probes exactly what create-prd declared. */
  prdSlice: z.string().min(1),

  /** Port the dotnet host will bind to. Defaults to 5000 (ASP.NET default). */
  backendPort: z.number().int().positive().default(5000),

  /** Port Vite will bind to. Defaults to 3000 (SmartStack convention). */
  frontendPort: z.number().int().positive().default(3000),

  /** Maximum wait per health-check (ms). Beyond this the smoke test
   *  reports `success=false` with reason `boot-timeout`. */
  timeoutMs: z.number().int().positive().default(60_000),

  /** When true, skip the dotnet boot and frontend boot ; only run the
   *  static URL derivation. Useful for unit-testing the URL extraction
   *  without spawning processes. */
  dryRun: z.boolean().default(false),

  /**
   * When true, the smoke test assumes `dotnet run` + `npm run dev` are
   * already up (legacy mode — the orchestrator manages them externally).
   * When false (the new default after Wave 2.3, 2026-05-27), `runSmoke`
   * spawns them itself, polls the health endpoints, runs the probes,
   * then tears the processes down via SIGTERM/Ctrl-C.
   */
  selfBoot: z.boolean().default(true),

  /**
   * When true, the orchestrator has already run `audit-dev-wire` and the
   * static gate passed. Short-circuits the runtime probe phase if the
   * caller wants this stricter ordering.
   */
  requireStaticPass: z.boolean().default(false),
});

export type RunSmokeArgs = z.infer<typeof RunSmokeArgsSchema>;

// ─── Report ─────────────────────────────────────────────────────────────

export interface SmokeProbe {
  /** Where the URL came from : `pageSpec` (frontend route inferred from
   *  pageSpec.filePath) or `apiClient` (URL string extracted from a
   *  generated service / hook file). */
  source: 'pageSpec' | 'apiClient';
  /** Path relative to the host (e.g. `/budgets/budgets/{id}` or
   *  `/api/budgets/dashboard/alerts`). */
  url: string;
  verb: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  /** Optional metadata pointing back to the artefact that declared this
   *  URL (file path + line). The dispatcher reads this to route a 404
   *  back to the right fix-skill. */
  expectedBy?: string;
}

export interface SmokeProbeResult extends SmokeProbe {
  status: number | null; // null when the request never completed
  durationMs: number;
  failed: boolean;
  /** Reason when failed=true. Possible values :
   *   - `status-4xx` / `status-5xx`
   *   - `network-error` (connection refused, DNS, etc.)
   *   - `timeout`
   *   - `skipped-auth` (endpoint requires auth, probed anonymously) */
  failureReason?: string;
}

export interface SmokeReport {
  /** True when zero probes failed (excluding skipped). */
  passed: boolean;
  backendBootMs: number | null;
  frontendBootMs: number | null;
  totalProbes: number;
  passedProbes: number;
  failedProbes: number;
  skippedProbes: number;
  probes: SmokeProbeResult[];
  /** Convenience subset — failed probes only. The dispatcher fans these
   *  out to the right fix-skill via `expectedBy`. */
  failures: SmokeProbeResult[];
}
