/**
 * uat-plan/types.ts — Input contract + discovery data-model for the `.plantest.yml`
 * generator.
 *
 * Two layers live here:
 *   1. UatPlanInputSchema — the `--spec` a caller passes (project + path scope + options).
 *   2. The DISCOVERY data-model — the plain-data result that the I/O layer (discover.ts
 *      + discover-endpoints.ts) produces and the PURE layer (project-roles.ts +
 *      generate.ts) consumes. Keeping discovery as plain data is what makes the
 *      generator deterministic and the plan byte-stable under a fixture (no live DB).
 */
import { z } from 'zod';

// ── Input contract ────────────────────────────────────────────────────────────

const ExecutionModeSchema = z.enum(['bfs', 'goto']);

/** Partial caps override — any omitted field falls back to the schema default. */
const CapsOverrideSchema = z
  .object({
    tabs: z.number().int(),
    buttons: z.number().int(),
    forms: z.number().int(),
    depth: z.number().int(),
    actions_per_role: z.number().int(),
  })
  .partial();

export const UatPlanInputSchema = z.object({
  /** Root of the generated SmartStack client app under test. */
  projectPath: z.string().min(1),
  /** Scope: `Application[/Module[/Section[/Resource]]]`. A partial path expands the sub-tree. */
  path: z.string().min(1),
  /** Plan name (artifact basename). Default: last path segment, or `all`. */
  name: z.string().optional(),
  /** Override the discovered role set (otherwise read from core.auth_Roles). */
  roles: z.array(z.string().min(1)).optional(),
  /** Execution modes written into the plan. Default: both. */
  modes: z.array(ExecutionModeSchema).optional(),
  /** Discover + emit the API axis (`endpoints[]`). Default true (D4 — v1). */
  includeApi: z.boolean().default(true),
  /** Partial caps override. */
  caps: CapsOverrideSchema.optional(),
  /** Override the artifact directory. Default: `.application-test/uat/{application}`. */
  outDir: z.string().optional(),
  /** Explicit SQL connection string. Default: resolved from the app's appsettings. */
  connectionString: z.string().optional(),
  /** Injectable ISO timestamp for `meta.generated_at` — fixed in tests for byte-stability. */
  generatedAt: z.string().optional(),
});

export type UatPlanInput = z.infer<typeof UatPlanInputSchema>;
export type CapsOverride = z.infer<typeof CapsOverrideSchema>;

// ── Discovery data-model (plain data — produced by the I/O layer) ───────────────

/**
 * A registered page "view" for a nav node. `list`/`section-home`/`module-home`
 * render the node's base route; `detail`/`edit`/`create` are the implicit-suffix
 * pages (`/:id`, `/:id/edit`, `/create`) DynamicRouter resolves from the dotted
 * component key (see scaffold-routes). Unknown suffixes are carried verbatim and
 * treated as a plain `goto` page.
 */
export type ImplicitView =
  | 'list'
  | 'section-home'
  | 'module-home'
  | 'detail'
  | 'edit'
  | 'create'
  | (string & {});

/** A nav node (application/module/section/resource) discovered from the runtime DB. */
export type NavKind = 'application' | 'module' | 'section' | 'resource';

/**
 * One navigable nav node in scope, correlated with the registered views the
 * frontend actually exposes for it (from componentRegistry). project-roles expands
 * this into one plan Route per view.
 */
export interface DiscoveredNavRoute {
  /** Dotted component key, e.g. "administration.users". */
  componentKey: string;
  /** Base URL for the node, e.g. "/administration/users" (the nav node's Route). */
  baseRoute: string;
  /** Human label (for `expect.title` hinting / reporting). */
  label?: string;
  /**
   * Required permission to reach the node's pages, resolved against the live
   * permission set (core.nav_Permissions). Undefined => no permission gate.
   */
  permission?: string;
  navKind: NavKind;
  /** Views registered for this node (always includes the base list/section-home). */
  views: ImplicitView[];
  /** Parent node component key (for sub-resources). */
  parentComponentKey?: string;
}

/** One controller action discovered from `[Http*]` + `[RequirePermission]` + `[NavRoute]`. */
export interface DiscoveredEndpoint {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  /** API route, e.g. "/api/administration/users" (class [Route] + method template). */
  route: string;
  controller?: string;
  /** Permission required by the action ([RequirePermission(...)]), if any. */
  permission?: string;
  /** Controller [NavRoute("a.b")], if present — ties the endpoint to a nav path. */
  navRoute?: string;
  /**
   * Declared success status, read from the action's `[ProducesResponseType(...,
   * StatusCodes.Status2xx)]`. Default 200; resource creates declare 201.
   */
  okStatus?: number;
  /** Action is `[AllowAnonymous]` — no auth; every role (incl. anonymous) gets okStatus. */
  allowAnonymous?: boolean;
}

/** Role grants in the rbac-matrix shape (path + wildcard), keyed by role name. */
export interface RbacData {
  /** Discovered role names, already canonically ordered (anonymous last). */
  roles: string[];
  grantsByRole: Record<string, { path: string; isWildcard: boolean }[]>;
}

/** The SHA triple embedded into the plan + the sidecar `.signature` (drift detection). */
export interface SourceSignature {
  nav_sha: string;
  rbac_sha: string;
  registry_sha: string;
}

/** Everything the pure layer needs to assemble a plan — produced by discover*(). */
export interface DiscoveryResult {
  /** Application code (first path segment). */
  application: string;
  /** The scope path the discovery was filtered to. */
  path: string;
  routes: DiscoveredNavRoute[];
  endpoints: DiscoveredEndpoint[];
  rbac: RbacData;
  signature: SourceSignature;
}

/** A file the generator emits (relative path + content). */
export interface GeneratedFile {
  /** Relative to projectPath, POSIX slashes. */
  path: string;
  content: string;
}
