/**
 * Shared universal-deps Proxy for tests that load real route modules with stub
 * dependencies. Any property access returns a sensible value:
 *   - asyncHandler (the most common trap): pass-through returning its argument
 *     so `router.get('/path', asyncHandler(realHandler))` resolves to
 *     `router.get('/path', realHandler)` and Express sees a real handler
 *   - Other functions: noopFn returning undefined when called
 *   - Objects: recursive proxy
 *
 * Used by:
 *   - depth2-routes-smoke.test.js (verifies every depth-2 route module loads)
 *   - public-routes-drift.test.js (walks aggregator routers via Express stack)
 */
const noopFn = () => undefined;
const passThrough = (x) => x;

// Logger-shaped noop: matches the real Logger's surface (error/warn/info/debug),
// so factories that do `log.error('tag', 'msg', meta)` or `(ctx.log || console).error(...)`
// don't blow up when run with stub deps. A bare `() => undefined` would throw because
// `noopFn.error` is undefined.
const loggerStub = { error: noopFn, warn: noopFn, info: noopFn, debug: noopFn, audit: noopFn };

const handler = {
  get(target, prop, receiver) {
    if (prop === 'asyncHandler') {
      // asyncHandler is special — it must accept a handler function and return
      // a wrapped handler function. Return a pass-through that wraps nothing.
      // This is the most common trap: `router.get('/path', asyncHandler(realHandler))`
      // resolves to `router.get('/path', realHandler)` and Express sees a real handler.
      return (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
    }
    if (prop === Symbol.toPrimitive) return undefined;
    if (prop === 'then') return undefined; // don't make the proxy thenable
    if (prop in target) return target[prop];
    // Functions and methods — return noopFn that returns undefined when called
    if (typeof target[prop] === 'function') return target[prop];
    return noopFn;
  },
  // Object.assign / spread / Object.keys on the proxy only sees the target's
  // OWN enumerable keys. Without these traps, aggregator factories that copy
  // ctx into a subCtx via `Object.assign({}, ctx, { helpers })` lose the
  // proxy's magic (e.g. asyncHandler), and downstream factories fail with
  // 'asyncHandler is not a function'. Expose all seed keys as own enumerable
  // so they survive the copy.
  ownKeys(target) {
    return Reflect.ownKeys(target);
  },
  getOwnPropertyDescriptor(target, prop) {
    if (prop in target) return Object.getOwnPropertyDescriptor(target, prop);
    return undefined;
  }
};

// Seed the proxy with a few known-shape fields so modules that destructure
// them get the right type. Anything else falls back to noopFn via the handler.
const seed = {
  fetchT: async () => ({ ok: true, status: 200, json: async () => ({}) }),
  // asyncHandler is special — see handler.get below. We also seed it as an
  // own enumerable property so Object.assign({}, ctx, { helpers }) copies it
  // through (the proxy's ownKeys trap only exposes own keys, so anything not
  // in the seed is invisible to spread/assign even though the get trap returns it).
  asyncHandler: (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next),
  servicesStateManager: {
    read: async () => [],
    write: async () => {},
    update: async () => []
  },
  siteConfig: { tld: '.home', dnsServers: {}, pylon: null },
  buildServiceUrl: (id) => `https://${id}.sami}`,
  logError: async () => undefined,
  // Logger-shaped stub (not a bare noopFn) so `(ctx.log || console).error(...)`
  // and `log.error('tag','msg',meta)` calls don't throw. See loggerStub above.
  log: loggerStub,
  errorResponse: noopFn,
  healthChecker: {
    getCurrentStatus: () => ({}),
    getServiceStats: () => null,
    configureService: noopFn,
    removeService: noopFn,
    getOpenIncidents: () => [],
    getIncidentHistory: () => []
  },
  authManager: {},
  credentialManager: {
    store: async () => undefined,
    retrieve: async () => null,
    diagnose: async () => ({ status: 'missing' }),
    rotateKey: async () => undefined
  },
  totpConfig: {
    isSetUp: false,
    enabled: false,
    sessionDuration: 'never',
    getConfig: () => ({}),
    saveConfig: async () => undefined
  },
  saveTotpConfig: async () => undefined,
  session: {
    create: async () => ({}),
    invalidate: async () => undefined,
    isValid: () => true
  },
  licenseManager: {
    requirePremium: () => (req, res, next) => next(),
    hasFeature: () => false
  },
  getServiceById: () => null,
  getAppSession: () => null,
  appSessionCache: { get: () => null, set: noopFn },
  renewCSRFToken: () => 'csrf-token',
  createCache: () => ({ get: () => null, set: noopFn }),
  CACHE_CONFIGS: {},
  docker: {},
  notification: { send: noopFn },
  buildDomain: (s) => s,
  caddy: {},
  addServiceToConfig: async () => undefined,
  APP_TEMPLATES: {},
  DOCKER: {}, REGEX: {}, TIMEOUTS: {}, APP: {}, PLEX: {}, LIMITS: {},
  SESSION_TTL: 86400,
  buildMediaAuth: () => ({}),
  CADDY: {},
  DEFAULT_DNS_PORT: '5380',
  isValidPort: () => true,
  exists: async () => true,
  validateURL: () => true,
  validateToken: () => true,
  validateAndLogConfig: () => ({}),
  validateConfig: () => ({ valid: true, errors: [], warnings: [] }),
  ValidationError: class extends Error {},
  AuthenticationError: class extends Error {},
  ForbiddenError: class extends Error {},
  NotFoundError: class extends Error {},
  ok: noopFn,
  successMessage: noopFn,
  validationError: noopFn,
  notFound: noopFn,
  error: noopFn,
  platformPaths: {},
  RECIPE_TEMPLATES: {},
  RECIPE_CATEGORIES: [],
  ARR_SERVICES: {},
  APP_PORTS: {},
  cryptoUtils: { encrypt: async (x) => x, decrypt: async (x) => x },
  // Path-like strings for routes that do `path.dirname(SERVICES_FILE)` etc
  // before the factory body runs (e.g. routes/config/backup.js). Bare noopFn
  // would throw 'path argument must be of type string. Received function'.
  SERVICES_FILE: '/tmp/dashcaddy/services.json',
  CONFIG_FILE: '/tmp/dashcaddy/config.json',
  TOTP_CONFIG_FILE: '/tmp/dashcaddy/totp.json',
  TAILSCALE_CONFIG_FILE: '/tmp/dashcaddy/tailscale.json',
  NOTIFICATIONS_FILE: '/tmp/dashcaddy/notifications.json',
  // Aggregator convenience: factories pass ctx.X into sub-router mounts;
  // some sub-routers destructure these by name. Seed-as-own-property so
  // Object.assign({}, ctx, { helpers }) copies them through.
  loadSiteConfig: async () => ({}),
  loadNotificationConfig: async () => ({}),
  configStateManager: { read: async () => ({}), write: async () => undefined, update: async () => undefined },
  readConfig: async () => ({}),
  saveConfig: async () => undefined,
  helpers: {},
  safeErrorMessage: (e) => (e && e.message) || 'Unknown error'
};

module.exports = { universalDeps: new Proxy(seed, handler), noopFn, passThrough };