/**
 * DC-057: dead-shadow /backups/schedule handler removed.
 *
 * The duplicate `router.post('/backups/schedule', ...)` previously registered
 * far below the canonical one was unreachable (Express matches the first
 * registered handler per METHOD+PATH). It bypassed `premiumGating` and
 * `validateBody` and used a `name`-keyed schema that would have corrupted the
 * backup config if it ever ran. The canonical handler uses the error code
 * `backups-schedule-update`; the dead handler used `backups-schedule-legacy`.
 * This test proves:
 *
 *   1. The router registers exactly ONE POST /backups/schedule handler
 *      (the canonical, appId-keyed one).
 *   2. No handler references the legacy "backups-schedule-legacy" error code.
 *   3. The legacy "name"-keyed schema now produces a 400 ValidationError
 *      from the canonical Joi schema (dead handler is gone).
 *   4. The canonical appId-keyed schema still succeeds (200).
 *   5. premiumGating is enforced on the canonical POST.
 *
 * Mirrors the audit-log.routes.test.js pattern.
 */

const express = require('express');

function buildFakeBackupManager() {
  const config = { backups: {}, defaultRetention: { keep: 7 } };
  return {
    getConfig: jest.fn(() => config),
    updateConfig: jest.fn((next) => {
      config.backups = next.backups || {};
    }),
    getHistory: jest.fn(() => []),
    restoreBackup: jest.fn(async (id) => {
      // Suppress require-await — keep async shape for parity with the
      // real backupManager.restoreBackup contract.
      return Promise.resolve({ id, status: 'restored' });
    }),
  };
}

function buildFakeLicenseManager() {
  const requirePremium = jest.fn(() => (_req, _res, next) => next());
  return {
    requirePremium,
    isPremium: jest.fn(() => true),
  };
}

function buildRouter(licenseManager, backupManager) {
  // Reset module cache so each test starts fresh
  jest.resetModules();
  const mod = require('../../routes/backups');
  return mod({
    backupManager,
    licenseManager,
    asyncHandler: (fn) => async (req, res, next) => { // eslint-disable-line require-await
      try { await fn(req, res, next); } catch (e) { next(e); }
    },
  });
}

function buildApp(router) {
  // Catch-all error handler so ValidationError / NotFoundError become JSON
  const app = express();
  app.use(express.json());
  app.use((req, res, next) => {
    // intentionally strip auth — the test does not exercise it
    next();
  });
  app.use('/', router);
  // eslint-disable-next-line no-unused-vars
  app.use((err, req, res, next) => {
    const status = err.statusCode || err.status || 500;
    res.status(status).json({
      error: err.message,
      code: err.code || 'ERR',
    });
  });
  return app;
}

function supertestFetch(app) {
  // Tiny in-process fetch helper (no need to add supertest dep)
  const http = require('http');
  return function (method, path, body) {
    return new Promise((resolve, reject) => {
      const server = app.listen(0, () => {
        const { port } = server.address();
        const data = body ? JSON.stringify(body) : null;
        const req = http.request({
          method,
          hostname: '127.0.0.1',
          port,
          path,
          headers: data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {},
        }, (res) => {
          let chunks = '';
          res.on('data', (c) => { chunks += c; });
          res.on('end', () => {
            server.close();
            let parsed;
            try { parsed = JSON.parse(chunks); } catch { parsed = chunks; }
            resolve({ status: res.statusCode, body: parsed });
          });
        });
        req.on('error', (e) => { server.close(); reject(e); });
        if (data) req.write(data);
        req.end();
      });
    });
  };
}

describe('routes/backups POST /backups/schedule (DC-057)', () => {
  let backupManager, licenseManager, app, fetch;

  beforeEach(() => {
    backupManager = buildFakeBackupManager();
    licenseManager = buildFakeLicenseManager();
    const router = buildRouter(licenseManager, backupManager);
    app = buildApp(router);
    fetch = supertestFetch(app);
  });

  test('registers exactly ONE POST /backups/schedule handler (canonical)', () => {
    // Inspect the registered router layers and confirm only one POST /backups/schedule
    // route exists (no shadowed / unreachable duplicate).
    const router = buildRouter(licenseManager, backupManager);
    const seen = [];
    router.stack.forEach((layer) => {
      if (layer.route && layer.route.path === '/backups/schedule' && layer.route.methods.post) {
        seen.push(layer.route);
      }
    });
    expect(seen).toHaveLength(1);
  });

  test('no handler references the legacy "backups-schedule-legacy" error code', () => {
    // The canonical handler uses error code 'backups-schedule-update'.
    // Walk the router stack and assert no route uses the legacy error code.
    const router = buildRouter(licenseManager, backupManager);
    const handlerStrings = [];
    function walk(node) {
      if (!node) return;
      if (node.stack) node.stack.forEach(walk);
      if (node.handle) {
        const code = node.handle.toString();
        handlerStrings.push(code);
      }
    }
    walk(router);
    const all = handlerStrings.join('\n');
    expect(all).not.toContain('backups-schedule-legacy');
  });

  test('legacy name-keyed schema is REJECTED with 400 (dead route truly gone)', async () => {
    // The dead handler accepted { name, schedule, maxStorageBytes, ...backupConfig }.
    // After removal, the canonical Joi schema (backupScheduleCreate) rejects this
    // shape because it requires `appId`. So we expect a 400.
    const res = await fetch('POST', '/backups/schedule', {
      name: 'mybackup',
      schedule: 'daily',
      maxStorageBytes: 1024,
    });
    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/appId.*required|appId is required/i);
  });

  test('canonical appId-keyed schema SUCCEEDS (200) and writes backup config', async () => {
    const res = await fetch('POST', '/backups/schedule', {
      appId: 'plex',
      schedule: 'daily',
      retention: { keep: 7 },
      destination: 'local',
      destinationPath: '/var/backups/plex',
      maxStorageBytes: 1024,
    });
    expect(res.status).toBe(200);
    expect(res.body.success).toBe(true);
    expect(backupManager.updateConfig).toHaveBeenCalledTimes(1);
    const written = backupManager.updateConfig.mock.calls[0][0];
    expect(written.backups).toHaveProperty('plex');
    expect(written.backups.plex.schedule).toBe('daily');
    expect(written.backups.plex.enabled).toBe(true);
    expect(written.backups.plex.maxStorageBytes).toBe(1024);
  });

  test('premium gating is enforced on POST /backups/schedule', async () => {
    // Replace the premium gate with one that 403s, then verify it runs.
    licenseManager.requirePremium.mockReturnValueOnce(
      (_req, res) => res.status(403).json({ error: 'premium required' }),
    );
    const router = buildRouter(licenseManager, backupManager);
    app = buildApp(router);
    fetch = supertestFetch(app);
    const res = await fetch('POST', '/backups/schedule', {
      appId: 'plex',
      schedule: 'daily',
    });
    expect(res.status).toBe(403);
    expect(backupManager.updateConfig).not.toHaveBeenCalled();
  });

  test('GET /backups/schedule still works (no collateral damage)', async () => {
    const res = await fetch('GET', '/backups/schedule');
    expect(res.status).toBe(200);
    expect(res.body.success).toBe(true);
    expect(res.body).toHaveProperty('schedules');
  });

  test('DELETE /backups/schedule/:appId still works', async () => {
    // Seed the config so the delete has something to remove
    backupManager.getConfig().backups.plex = { schedule: 'daily' };
    const res = await fetch('DELETE', '/backups/schedule/plex');
    expect(res.status).toBe(200);
    expect(backupManager.updateConfig).toHaveBeenCalled();
  });
});
