/**
 * Smoke tests for notification-manager.js
 * Verifies the NotificationManager loads, exposes the expected interface,
 * handles config loading/saving, sends notifications via providers, and
 * correctly tracks history.
 */

jest.mock('fs', () => ({
  existsSync: jest.fn().mockReturnValue(false),
  readFileSync: jest.fn().mockReturnValue('{}'),
  writeFileSync: jest.fn(),
  mkdirSync: jest.fn(),
}));

jest.mock('nodemailer', () => ({
  createTransport: jest.fn(() => ({
    sendMail: jest.fn().mockResolvedValue({ messageId: 'mock' }),
  })),
}));

const fs = require('fs');
const nodemailer = require('nodemailer');
const NotificationManager = require('../src/managers/notification-manager');

describe('NotificationManager', () => {
  let nm;
  const NOTIF_FILE = '/tmp/dc-notif-test.json';

  beforeEach(() => {
    jest.clearAllMocks();
    fs.existsSync.mockReturnValue(false);
    fs.readFileSync.mockReturnValue('{}');
    fs.writeFileSync.mockReturnValue(undefined);
    fs.mkdirSync.mockReturnValue(undefined);

    nm = new NotificationManager({
      NOTIFICATIONS_FILE: NOTIF_FILE,
      log: { error: jest.fn(), info: jest.fn(), warn: jest.fn() },
      fetchT: jest.fn(),
      docker: null,
    });
  });

  afterEach(() => {
    nm.stopHealthDaemon();
  });

  test('initializes with default config', () => {
    const cfg = nm.getConfig();
    expect(cfg.enabled).toBe(true);
    expect(cfg.providers).toHaveProperty('discord');
    expect(cfg.providers).toHaveProperty('telegram');
    expect(cfg.providers).toHaveProperty('ntfy');
    expect(cfg.providers).toHaveProperty('email');
  });

  test('starts with empty history and null lastSent', () => {
    expect(nm.getHistory()).toEqual([]);
    expect(nm.lastSent).toBeNull();
  });

  test('saveConfig writes the config to disk and creates parent dir', async () => {
    fs.existsSync.mockReturnValue(false);
    await nm.saveConfig();
    expect(fs.mkdirSync).toHaveBeenCalled();
    expect(fs.writeFileSync).toHaveBeenCalled();
    const callArgs = fs.writeFileSync.mock.calls[0];
    expect(callArgs[0]).toBe(NOTIF_FILE);
    expect(callArgs[1]).toContain('enabled');
  });

  test('loadConfig merges file content with defaults', () => {
    fs.existsSync.mockReturnValue(true);
    fs.readFileSync.mockReturnValue(JSON.stringify({ enabled: false }));
    const loaded = new NotificationManager({
      NOTIFICATIONS_FILE: NOTIF_FILE,
      log: { error: jest.fn(), info: jest.fn(), warn: jest.fn() },
    });
    expect(loaded.getConfig().enabled).toBe(false);
  });

  test('clearHistory empties the history array', () => {
    nm.history.push({ event: 'test', timestamp: new Date().toISOString() });
    expect(nm.getHistory().length).toBe(1);
    nm.clearHistory();
    expect(nm.getHistory().length).toBe(0);
  });

  test('send returns disabled when notifications are off', async () => {
    nm.config.enabled = false;
    const result = await nm.send('alert', { text: 'hi' });
    expect(result.success).toBe(false);
    expect(result.error).toMatch(/disabled/i);
  });

  test('send returns event-not-enabled for unknown events', async () => {
    nm.config.events['some-disabled-event'] = false;
    const result = await nm.send('some-disabled-event', { text: 'hi' });
    expect(result.success).toBe(false);
    expect(result.error).toMatch(/not enabled/i);
  });

  test('send with no providers enabled records history and returns success:false', async () => {
    const result = await nm.send('alert', { text: 'hello' });
    expect(result).toHaveProperty('results');
    expect(Array.isArray(result.results)).toBe(true);
    expect(nm.getHistory().length).toBe(1);
    expect(nm.getHistory()[0].event).toBe('alert');
  });

  test('sendDiscord calls ctx.fetchT and returns success on 2xx', async () => {
    nm.config.providers.discord = { enabled: true, webhookUrl: 'https://hook.test/x' };
    nm.ctx.fetchT = jest.fn().mockResolvedValue({ ok: true });
    const result = await nm.sendDiscord('msg', { title: 'T' });
    expect(result.success).toBe(true);
    expect(nm.ctx.fetchT).toHaveBeenCalledWith(
      'https://hook.test/x',
      expect.objectContaining({ method: 'POST' })
    );
  });

  test('sendDiscord throws on non-2xx response', async () => {
    nm.config.providers.discord = { enabled: true, webhookUrl: 'https://hook.test/x' };
    nm.ctx.fetchT = jest.fn().mockResolvedValue({ ok: false, status: 500 });
    await expect(nm.sendDiscord('msg', null)).rejects.toThrow(/Discord/);
  });

  test('sendTelegram calls Telegram API', async () => {
    nm.config.providers.telegram = { enabled: true, botToken: 'TOK', chatId: '123' };
    nm.ctx.fetchT = jest.fn().mockResolvedValue({ json: () => Promise.resolve({ ok: true }) });
    const result = await nm.sendTelegram('hello');
    expect(result.success).toBe(true);
    expect(nm.ctx.fetchT).toHaveBeenCalledWith(
      expect.stringContaining('api.telegram.org'),
      expect.objectContaining({ method: 'POST' })
    );
  });

  test('sendNtfy posts to the configured serverUrl + topic', async () => {
    nm.config.providers.ntfy = { enabled: true, topic: 'dashcaddy', serverUrl: 'https://ntfy.sh' };
    nm.ctx.fetchT = jest.fn().mockResolvedValue({ ok: true });
    const result = await nm.sendNtfy('body', 'title');
    expect(result.success).toBe(true);
    expect(nm.ctx.fetchT).toHaveBeenCalledWith(
      'https://ntfy.sh/dashcaddy',
      expect.objectContaining({ method: 'POST' })
    );
  });

  test('sendEmail uses nodemailer transporter', async () => {
    nm.config.providers.email = {
      enabled: true,
      host: 'smtp.test',
      port: 587,
      to: 'me@test',
      from: 'from@test',
      username: 'u',
      password: 'p',
    };
    const result = await nm.sendEmail('subject', 'body');
    expect(result.success).toBe(true);
    expect(nodemailer.createTransport).toHaveBeenCalled();
  });

  test('sendAlert, sendBackupComplete, sendServiceEvent do not throw', async () => {
    const alertResult = await nm.sendAlert({
      containerName: 'web',
      alerts: [{ type: 'cpu', severity: 'warning', message: 'high' }],
      timestamp: new Date().toISOString(),
    });
    expect(alertResult).toBeDefined();

    const backupResult = await nm.sendBackupComplete({
      name: 'daily',
      status: 'success',
    });
    expect(backupResult).toBeDefined();

    const serviceResult = await nm.sendServiceEvent('container-down', {
      name: 'web',
      containerName: 'sami-web',
    });
    expect(serviceResult).toBeDefined();
  });

  test('checkHealth returns checked:false when no docker client', async () => {
    nm.ctx.docker = null;
    const r = await nm.checkHealth();
    expect(r.checked).toBe(false);
  });

  test('checkHealth with mocked docker returns checked:true', async () => {
    nm.ctx.docker = {
      listContainers: jest.fn().mockResolvedValue([
        { Id: 'aaaabbbbcccc', Names: ['/web'], State: 'running', Status: 'Up' },
        { Id: 'ddddeeeeffff', Names: ['/api'], State: 'exited', Status: 'Exited' },
      ]),
    };
    nm.config.healthCheck = { enabled: true, intervalMinutes: 5 };
    const r = await nm.checkHealth();
    expect(r.checked).toBe(true);
    expect(r.containersMonitored).toBe(2);
  });

  test('formatTitle returns a string for known events', () => {
    expect(typeof nm._formatTitle('alert')).toBe('string');
    expect(typeof nm._formatTitle('unknown')).toBe('string');
  });

  test('startHealthDaemon and stopHealthDaemon are idempotent', () => {
    nm.startHealthDaemon();
    nm.startHealthDaemon(); // should not double-schedule
    nm.stopHealthDaemon();
    nm.stopHealthDaemon();
    expect(nm.healthDaemonInterval).toBeNull();
  });
});
