/**
 * DC-092: notifications config contract tests (route level).
 *
 * The settings UI and the backend drifted apart in three ways, all of which
 * made user-facing features silently dead:
 *   1. UI sent email.user/email.pass; backend read username/password →
 *      SMTP auth never applied for UI-saved configs.
 *   2. UI sent camelCase event keys (containerDown); the send() gate read
 *      kebab-case keys (container-down) → event toggles were cosmetic.
 *   3. deploy-success/deploy-failed/auto-restart were missing from DEFAULT
 *      events → deploy + auto-restart notifications always dropped, and
 *      'test' was gated too → the Test button was a no-op.
 *   4. Non-boolean enabled/secure values (string "false") persisted as-is and
 *      coerced truthy (!!secure) — silently forcing TLS.
 *   5. UI password field roundtrip: GET /config omitted port/secure/to/
 *      username, and an empty password on save clobbered the stored one.
 *
 * These tests pin the FIXED contract: alias normalization, strict booleans,
 * event-key folding, non-destructive credential merge, redacted GET fields.
 */

'use strict';

const express = require('express');
const request = require('supertest');

// Stub notification manager: in-memory config object, real merge semantics
// are exercised through the route; manager-level canonicalization has its
// own tests in notification-manager.test.js.
function makeStubNotification(initial) {
  const nm = {
    config: initial,
    getConfig() { return this.config; },
    async saveConfig() { this.saved = JSON.parse(JSON.stringify(this.config)); return true; },
    startHealthDaemon: jest.fn(),
    stopHealthDaemon: jest.fn(),
  };
  return nm;
}

function buildApp(notification) {
  const app = express();
  app.use(express.json());
  const notificationRoutes = require('../../routes/notifications');
  app.use('/api/v1/notifications', notificationRoutes({
    notification,
    asyncHandler: (fn) => (req, res, next) => Promise.resolve(fn(req, res)).catch(next),
    ok: (res, data) => res.json({ success: true, ...data }),
  }));
  // Inline error handler (same pattern as sites-dc074.routes.test.js): maps
  // AppError.statusCode to the HTTP status and surfaces err.message.
  app.use((err, req, res, next) => {
    const status = err.statusCode || 500;
    res.status(status).json({
      error: err.message || 'Internal Server Error',
      code: err.code || null,
    });
  });
  return app;
}

const DEFAULTS = {
  enabled: true,
  providers: {
    discord: { enabled: false, webhookUrl: '' },
    telegram: { enabled: false, botToken: '', chatId: '' },
    ntfy: { enabled: false, topic: '', serverUrl: 'https://ntfy.sh' },
    email: { enabled: false, host: '', port: 587, to: '', from: '', username: '', password: '' },
  },
  events: {
    'container-down': true,
    'container-up': false,
    'alert': true,
    'backup-complete': true,
    'backup-failed': true,
    'update-available': true,
    'deploy-success': true,
    'deploy-failed': true,
    'auto-restart': true,
  },
  healthCheck: { enabled: false },
};

function freshConfig() {
  return JSON.parse(JSON.stringify(DEFAULTS));
}

describe('DC-092: POST /config field aliases and typing', () => {
  test('UI spelling email.user/email.pass normalizes onto username/password', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    const res = await request(app)
      .post('/api/v1/notifications/config')
      .send({ providers: { email: { user: 'svc@example.com', pass: 'app-secret' } } });
    expect(res.status).toBe(200);
    expect(nm.config.providers.email.username).toBe('svc@example.com');
    expect(nm.config.providers.email.password).toBe('app-secret');
    expect(nm.config.providers.email.user).toBeUndefined();
    expect(nm.config.providers.email.pass).toBeUndefined();
  });

  test('explicit username/password wins over user/pass aliases', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    await request(app)
      .post('/api/v1/notifications/config')
      .send({ providers: { email: { user: 'legacy@x.com', pass: 'old', username: 'modern@x.com', password: 'new' } } });
    expect(nm.config.providers.email.username).toBe('modern@x.com');
    expect(nm.config.providers.email.password).toBe('new');
  });

  test('string "false" for secure is rejected, not coerced truthy', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    const res = await request(app)
      .post('/api/v1/notifications/config')
      .send({ providers: { email: { secure: 'false' } } });
    expect(res.status).toBe(400);
    expect(nm.config.providers.email.secure).toBeUndefined();
  });

  test('string enabled for any provider is rejected', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    for (const prov of ['discord', 'telegram', 'ntfy', 'email']) {
      const res = await request(app)
        .post('/api/v1/notifications/config')
        .send({ providers: { [prov]: { enabled: 'true' } } });
      expect(res.status).toBe(400);
    }
    const top = await request(app)
      .post('/api/v1/notifications/config')
      .send({ enabled: 'true' });
    expect(top.status).toBe(400);
  });

  test('real booleans pass and persist', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    const res = await request(app)
      .post('/api/v1/notifications/config')
      .send({ enabled: false, providers: { email: { secure: true } } });
    expect(res.status).toBe(200);
    expect(nm.config.enabled).toBe(false);
    expect(nm.config.providers.email.secure).toBe(true);
  });

  test('SMTP port bounds enforced (0, 65536, non-integer rejected)', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    for (const bad of [0, 65536, 58.5, 'abc']) {
      const res = await request(app)
        .post('/api/v1/notifications/config')
        .send({ providers: { email: { port: bad } } });
      expect(res.status).toBe(400);
    }
    const good = await request(app)
      .post('/api/v1/notifications/config')
      .send({ providers: { email: { port: 465 } } });
    expect(good.status).toBe(200);
    expect(nm.config.providers.email.port).toBe(465);
  });
});

describe('DC-092: POST /config event-key folding', () => {
  test('camelCase event keys fold onto canonical kebab keys', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    const res = await request(app)
      .post('/api/v1/notifications/config')
      .send({ events: { containerDown: false, deploymentSuccess: false, resourceAlert: false } });
    expect(res.status).toBe(200);
    expect(nm.config.events['container-down']).toBe(false);
    expect(nm.config.events['deploy-success']).toBe(false);
    expect(nm.config.events['alert']).toBe(false);
    // legacy camelCase keys must NOT be stored
    expect(nm.config.events.containerDown).toBeUndefined();
    expect(nm.config.events.deploymentSuccess).toBeUndefined();
    expect(nm.config.events.resourceAlert).toBeUndefined();
  });

  test('canonical kebab keys accepted directly', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    const res = await request(app)
      .post('/api/v1/notifications/config')
      .send({ events: { 'container-down': false, 'auto-restart': false } });
    expect(res.status).toBe(200);
    expect(nm.config.events['container-down']).toBe(false);
    expect(nm.config.events['auto-restart']).toBe(false);
  });

  test('non-boolean event values rejected', async () => {
    const nm = makeStubNotification(freshConfig());
    const app = buildApp(nm);
    const res = await request(app)
      .post('/api/v1/notifications/config')
      .send({ events: { 'container-down': 'yes' } });
    expect(res.status).toBe(400);
  });
});

describe('DC-092: POST /config non-destructive credential merge', () => {
  test('empty password does not clobber stored password', async () => {
    const cfg = freshConfig();
    cfg.providers.email.username = 'svc@example.com';
    cfg.providers.email.password = 'stored-secret';
    const nm = makeStubNotification(cfg);
    const app = buildApp(nm);
    const res = await request(app)
      .post('/api/v1/notifications/config')
      .send({ providers: { email: { host: 'smtp.example.com', password: '' } } });
    expect(res.status).toBe(200);
    expect(nm.config.providers.email.password).toBe('stored-secret');
    expect(nm.config.providers.email.host).toBe('smtp.example.com');
  });

  test('empty username does not clobber stored username', async () => {
    const cfg = freshConfig();
    cfg.providers.email.username = 'svc@example.com';
    const nm = makeStubNotification(cfg);
    const app = buildApp(nm);
    await request(app)
      .post('/api/v1/notifications/config')
      .send({ providers: { email: { username: '' } } });
    expect(nm.config.providers.email.username).toBe('svc@example.com');
  });

  test('non-empty password overwrites', async () => {
    const cfg = freshConfig();
    cfg.providers.email.password = 'old';
    const nm = makeStubNotification(cfg);
    const app = buildApp(nm);
    await request(app)
      .post('/api/v1/notifications/config')
      .send({ providers: { email: { password: 'rotated' } } });
    expect(nm.config.providers.email.password).toBe('rotated');
  });
});

describe('DC-092: GET /config redaction and roundtrip fields', () => {
  test('returns port/secure/to/username/hasPassword but never the password', async () => {
    const cfg = freshConfig();
    cfg.providers.email = {
      enabled: true,
      host: 'smtp.example.com',
      port: 465,
      secure: true,
      to: 'admin@example.com',
      from: 'DashCaddy <noreply@example.com>',
      username: 'svc@example.com',
      password: 'super-secret',
    };
    const nm = makeStubNotification(cfg);
    const app = buildApp(nm);
    const res = await request(app).get('/api/v1/notifications/config');
    expect(res.status).toBe(200);
    const email = res.body.config.providers.email;
    expect(email.port).toBe(465);
    expect(email.secure).toBe(true);
    expect(email.to).toBe('admin@example.com');
    expect(email.username).toBe('svc@example.com');
    expect(email.hasPassword).toBe(true);
    expect(JSON.stringify(res.body)).not.toContain('super-secret');
    expect(res.body.config.providers.email.password).toBeUndefined();
  });
});
