// Resource Monitor Tests
// Validates container CPU/memory/disk/network tracking, alerts, and persistence

jest.mock('dockerode');
jest.mock('fs');

const fs = require('fs');
const EventEmitter = require('events');

// Setup defaults BEFORE requiring singleton
fs.existsSync.mockReturnValue(false);
fs.readFileSync.mockReturnValue('{}');
fs.writeFileSync.mockReturnValue(undefined);

const resourceMonitor = require('../src/managers/resource-monitor');

function makeStat(overrides = {}) {
  return {
    timestamp: new Date().toISOString(),
    cpu: { percent: 15.5, usage: 500000 },
    memory: { usage: 536870912, limit: 2147483648, percent: 25.0, usageMB: 512, limitMB: 2048 },
    network: { rxBytes: 1048576, txBytes: 524288, rxMB: 1, txMB: 0.5 },
    disk: { readBytes: 0, writeBytes: 0, readMB: 0, writeMB: 0 },
    pids: 42,
    ...overrides
  };
}

beforeEach(() => {
  jest.clearAllMocks();
  jest.useFakeTimers();

  fs.existsSync.mockReturnValue(false);
  fs.readFileSync.mockReturnValue('{}');
  fs.writeFileSync.mockReturnValue(undefined);

  // Reset internal state
  resourceMonitor.stats.clear();
  resourceMonitor.alerts.clear();
  resourceMonitor.lastAlerts.clear();
  resourceMonitor.monitoring = false;
  if (resourceMonitor.monitoringInterval) {
    clearInterval(resourceMonitor.monitoringInterval);
    resourceMonitor.monitoringInterval = null;
  }
});

afterEach(() => {
  resourceMonitor.stop();
  jest.useRealTimers();
});

describe('ResourceMonitor — container resource tracking', () => {

  describe('start/stop lifecycle', () => {
    it('starts monitoring', () => {
      resourceMonitor.start();
      expect(resourceMonitor.monitoring).toBe(true);
    });

    it('ignores double start', () => {
      resourceMonitor.start();
      resourceMonitor.start();
      expect(resourceMonitor.monitoring).toBe(true);
    });

    it('stops monitoring and saves stats', () => {
      resourceMonitor.start();
      resourceMonitor.stop();
      expect(resourceMonitor.monitoring).toBe(false);
      expect(resourceMonitor.monitoringInterval).toBeNull();
      expect(fs.writeFileSync).toHaveBeenCalled();
    });

    it('ignores stop when not monitoring', () => {
      resourceMonitor.stop();
      expect(resourceMonitor.monitoring).toBe(false);
    });
  });

  describe('recordStats', () => {
    it('creates new entry for unknown container', () => {
      const stat = makeStat();
      resourceMonitor.recordStats('abc123', '/plex', stat);
      expect(resourceMonitor.stats.has('abc123')).toBe(true);
      expect(resourceMonitor.stats.get('abc123').history).toHaveLength(1);
    });

    it('appends to existing container history', () => {
      resourceMonitor.recordStats('abc123', '/plex', makeStat());
      resourceMonitor.recordStats('abc123', '/plex', makeStat());
      expect(resourceMonitor.stats.get('abc123').history).toHaveLength(2);
    });

    it('updates container name if changed', () => {
      resourceMonitor.recordStats('abc123', '/plex-old', makeStat());
      resourceMonitor.recordStats('abc123', '/plex-new', makeStat());
      expect(resourceMonitor.stats.get('abc123').name).toBe('/plex-new');
    });

    it('trims stats older than retention period', () => {
      const oldStat = makeStat({ timestamp: new Date(Date.now() - 999 * 60 * 60 * 1000).toISOString() });
      const newStat = makeStat();
      resourceMonitor.recordStats('abc123', '/plex', oldStat);
      resourceMonitor.recordStats('abc123', '/plex', newStat);
      // Old stat exceeds 168h (7 day) retention
      expect(resourceMonitor.stats.get('abc123').history).toHaveLength(1);
    });
  });

  describe('getCurrentStats', () => {
    it('returns null for unknown container', () => {
      expect(resourceMonitor.getCurrentStats('unknown')).toBeNull();
    });

    it('returns latest stat entry', () => {
      const stat1 = makeStat({ cpu: { percent: 10, usage: 100 } });
      const stat2 = makeStat({ cpu: { percent: 50, usage: 500 } });
      resourceMonitor.recordStats('abc123', '/plex', stat1);
      resourceMonitor.recordStats('abc123', '/plex', stat2);
      expect(resourceMonitor.getCurrentStats('abc123').cpu.percent).toBe(50);
    });
  });

  describe('getHistoricalStats', () => {
    it('returns empty array for unknown container', () => {
      expect(resourceMonitor.getHistoricalStats('unknown')).toEqual([]);
    });

    it('filters by time window', () => {
      const recentStat = makeStat();
      const oldStat = makeStat({ timestamp: new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString() });

      resourceMonitor.stats.set('abc123', {
        name: '/plex',
        history: [oldStat, recentStat]
      });

      // Only last 24 hours
      const result = resourceMonitor.getHistoricalStats('abc123', 24);
      expect(result).toHaveLength(1);
    });
  });

  describe('getAggregatedStats', () => {
    it('returns null for unknown container', () => {
      expect(resourceMonitor.getAggregatedStats('unknown')).toBeNull();
    });

    it('calculates min/max/avg for CPU and memory', () => {
      const stats = [
        makeStat({ cpu: { percent: 10, usage: 100 }, memory: { percent: 20, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } }),
        makeStat({ cpu: { percent: 30, usage: 300 }, memory: { percent: 40, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } }),
        makeStat({ cpu: { percent: 50, usage: 500 }, memory: { percent: 60, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } }),
      ];
      resourceMonitor.stats.set('abc123', { name: '/plex', history: stats });

      const agg = resourceMonitor.getAggregatedStats('abc123', 24);
      expect(agg.cpu.min).toBe(10);
      expect(agg.cpu.max).toBe(50);
      expect(agg.cpu.avg).toBe(30);
      expect(agg.cpu.current).toBe(50);
      expect(agg.memory.min).toBe(20);
      expect(agg.memory.max).toBe(60);
      expect(agg.dataPoints).toBe(3);
    });
  });

  describe('getAllStats', () => {
    it('returns all containers with current and aggregated data', () => {
      resourceMonitor.recordStats('abc123', '/plex', makeStat());
      resourceMonitor.recordStats('def456', '/radarr', makeStat());

      const all = resourceMonitor.getAllStats();
      expect(Object.keys(all)).toHaveLength(2);
      expect(all['abc123'].name).toBe('/plex');
      expect(all['abc123'].current).toBeDefined();
      expect(all['abc123'].aggregated).toBeDefined();
    });
  });

  describe('alert configuration', () => {
    it('setAlertConfig stores config and persists', () => {
      resourceMonitor.setAlertConfig('abc123', {
        cpuThreshold: 80,
        memoryThreshold: 90,
        cooldownMinutes: 30
      });

      const config = resourceMonitor.getAlertConfig('abc123');
      expect(config.enabled).toBe(true);
      expect(config.cpuThreshold).toBe(80);
      expect(config.memoryThreshold).toBe(90);
      expect(config.cooldownMinutes).toBe(30);
      expect(fs.writeFileSync).toHaveBeenCalled();
    });

    it('returns null for unconfigured container', () => {
      expect(resourceMonitor.getAlertConfig('unknown')).toBeNull();
    });

    it('removeAlertConfig clears config and cooldown', () => {
      resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 80 });
      resourceMonitor.lastAlerts.set('abc123', Date.now());
      resourceMonitor.removeAlertConfig('abc123');
      expect(resourceMonitor.getAlertConfig('abc123')).toBeNull();
      expect(resourceMonitor.lastAlerts.has('abc123')).toBe(false);
    });
  });

  describe('checkAlerts', () => {
    it('emits alert when CPU exceeds threshold', () => {
      const handler = jest.fn();
      resourceMonitor.on('alert', handler);

      resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 50, cooldownMinutes: 0 });
      const stat = makeStat({ cpu: { percent: 75, usage: 750 } });
      resourceMonitor.checkAlerts('abc123', '/plex', stat);

      expect(handler).toHaveBeenCalledWith(
        expect.objectContaining({
          containerId: 'abc123',
          alerts: expect.arrayContaining([
            expect.objectContaining({ type: 'cpu', value: 75 })
          ])
        })
      );
      resourceMonitor.off('alert', handler);
    });

    it('emits alert when memory exceeds threshold', () => {
      const handler = jest.fn();
      resourceMonitor.on('alert', handler);

      resourceMonitor.setAlertConfig('abc123', { memoryThreshold: 20, cooldownMinutes: 0 });
      const stat = makeStat({ memory: { percent: 80, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } });
      resourceMonitor.checkAlerts('abc123', '/plex', stat);

      expect(handler).toHaveBeenCalledWith(
        expect.objectContaining({
          alerts: expect.arrayContaining([
            expect.objectContaining({ type: 'memory' })
          ])
        })
      );
      resourceMonitor.off('alert', handler);
    });

    it('emits alert when disk I/O exceeds threshold', () => {
      const handler = jest.fn();
      resourceMonitor.on('alert', handler);

      resourceMonitor.setAlertConfig('abc123', { diskIOThreshold: 10, cooldownMinutes: 0 });
      const stat = makeStat({ disk: { readMB: 15, writeMB: 10, readBytes: 0, writeBytes: 0 } });
      resourceMonitor.checkAlerts('abc123', '/plex', stat);

      expect(handler).toHaveBeenCalledWith(
        expect.objectContaining({
          alerts: expect.arrayContaining([
            expect.objectContaining({ type: 'disk' })
          ])
        })
      );
      resourceMonitor.off('alert', handler);
    });

    it('respects cooldown period', () => {
      const handler = jest.fn();
      resourceMonitor.on('alert', handler);

      resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 50, cooldownMinutes: 15 });
      resourceMonitor.lastAlerts.set('abc123', Date.now()); // Just alerted

      const stat = makeStat({ cpu: { percent: 99, usage: 990 } });
      resourceMonitor.checkAlerts('abc123', '/plex', stat);

      expect(handler).not.toHaveBeenCalled();
      resourceMonitor.off('alert', handler);
    });

    it('skips when alerts not configured or disabled', () => {
      const handler = jest.fn();
      resourceMonitor.on('alert', handler);

      // No config
      resourceMonitor.checkAlerts('abc123', '/plex', makeStat());
      expect(handler).not.toHaveBeenCalled();

      // Disabled config
      resourceMonitor.alerts.set('abc123', { enabled: false, cpuThreshold: 1 });
      resourceMonitor.checkAlerts('abc123', '/plex', makeStat());
      expect(handler).not.toHaveBeenCalled();

      resourceMonitor.off('alert', handler);
    });

    it('does not alert when below thresholds', () => {
      const handler = jest.fn();
      resourceMonitor.on('alert', handler);

      resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 90, memoryThreshold: 90, cooldownMinutes: 0 });
      const stat = makeStat({ cpu: { percent: 5, usage: 50 }, memory: { percent: 10, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } });
      resourceMonitor.checkAlerts('abc123', '/plex', stat);

      expect(handler).not.toHaveBeenCalled();
      resourceMonitor.off('alert', handler);
    });
  });

  describe('cleanupOldStats', () => {
    it('removes containers with no recent data', () => {
      const oldStat = makeStat({ timestamp: new Date(Date.now() - 999 * 60 * 60 * 1000).toISOString() });
      resourceMonitor.stats.set('old-container', { name: '/old', history: [oldStat] });
      resourceMonitor.cleanupOldStats();
      expect(resourceMonitor.stats.has('old-container')).toBe(false);
    });

    it('keeps containers with recent data', () => {
      resourceMonitor.recordStats('abc123', '/plex', makeStat());
      resourceMonitor.cleanupOldStats();
      expect(resourceMonitor.stats.has('abc123')).toBe(true);
    });
  });

  describe('persistence (loadStats/saveStats)', () => {
    it('loadStats populates from file', () => {
      const savedData = {
        'abc123': { name: '/plex', history: [makeStat()] }
      };
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify(savedData));

      resourceMonitor.loadStats();
      expect(resourceMonitor.stats.size).toBe(1);
    });

    it('loadStats handles missing file', () => {
      fs.existsSync.mockReturnValue(false);
      resourceMonitor.loadStats();
      expect(resourceMonitor.stats.size).toBe(0);
    });

    it('loadStats handles corrupt file', () => {
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockImplementation(() => { throw new Error('corrupt'); });
      resourceMonitor.loadStats(); // should not throw
    });

    it('saveStats writes Map as JSON object', () => {
      resourceMonitor.recordStats('abc123', '/plex', makeStat());
      resourceMonitor.saveStats();
      expect(fs.writeFileSync).toHaveBeenCalledWith(
        expect.any(String),
        expect.stringContaining('abc123')
      );
    });

    it('saveStats handles write error', () => {
      fs.writeFileSync.mockImplementation(() => { throw new Error('disk full'); });
      resourceMonitor.recordStats('abc123', '/plex', makeStat());
      resourceMonitor.saveStats(); // should not throw
    });
  });

  describe('alert config persistence', () => {
    it('loadAlertConfig populates from file', () => {
      const config = { 'abc123': { enabled: true, cpuThreshold: 80 } };
      fs.existsSync.mockReturnValue(true);
      fs.readFileSync.mockReturnValue(JSON.stringify(config));

      resourceMonitor.loadAlertConfig();
      expect(resourceMonitor.alerts.size).toBe(1);
    });

    it('saveAlertConfig writes alerts as JSON', () => {
      resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 80 });
      expect(fs.writeFileSync).toHaveBeenCalled();
    });
  });

  describe('exportStats / importStats', () => {
    it('exports stats and alerts', () => {
      resourceMonitor.recordStats('abc123', '/plex', makeStat());
      resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 80 });

      const exported = resourceMonitor.exportStats();
      expect(exported.stats['abc123']).toBeDefined();
      expect(exported.alerts['abc123']).toBeDefined();
      expect(exported.exportedAt).toBeDefined();
    });

    it('imports stats and alerts', () => {
      const data = {
        stats: { 'abc123': { name: '/plex', history: [makeStat()] } },
        alerts: { 'abc123': { enabled: true, cpuThreshold: 90 } }
      };

      resourceMonitor.importStats(data);
      expect(resourceMonitor.stats.size).toBe(1);
      expect(resourceMonitor.alerts.size).toBe(1);
      // Should persist after import
      expect(fs.writeFileSync).toHaveBeenCalled();
    });
  });

  describe('getContainerStats (Docker stats parsing)', () => {
    it('parses Docker stats into structured format', async () => {
      const Docker = require('dockerode');
      const mockContainer = {
        stats: jest.fn((opts, cb) => cb(null, {
          cpu_stats: {
            cpu_usage: { total_usage: 200000 },
            system_cpu_usage: 1000000
          },
          precpu_stats: {
            cpu_usage: { total_usage: 100000 },
            system_cpu_usage: 500000
          },
          memory_stats: {
            usage: 536870912,  // 512MB
            limit: 2147483648  // 2GB
          },
          networks: {
            eth0: { rx_bytes: 1048576, tx_bytes: 524288 }
          },
          blkio_stats: {
            io_service_bytes_recursive: [
              { op: 'Read', value: 1048576 },
              { op: 'Write', value: 2097152 }
            ]
          },
          pids_stats: { current: 42 }
        }))
      };

      const result = await resourceMonitor.getContainerStats(mockContainer);
      expect(result.cpu.percent).toBe(20); // (100000/500000) * 100
      expect(result.memory.usageMB).toBe(512);
      expect(result.memory.limitMB).toBe(2048);
      expect(result.memory.percent).toBe(25);
      expect(result.network.rxMB).toBe(1);
      expect(result.disk.readMB).toBe(1);
      expect(result.disk.writeMB).toBe(2);
      expect(result.pids).toBe(42);
    });

    it('handles missing network stats', async () => {
      const mockContainer = {
        stats: jest.fn((opts, cb) => cb(null, {
          cpu_stats: { cpu_usage: { total_usage: 0 }, system_cpu_usage: 0 },
          precpu_stats: { cpu_usage: { total_usage: 0 }, system_cpu_usage: 0 },
          memory_stats: { usage: 0, limit: 0 },
          blkio_stats: {},
          pids_stats: {}
        }))
      };

      const result = await resourceMonitor.getContainerStats(mockContainer);
      expect(result.network.rxBytes).toBe(0);
      expect(result.network.txBytes).toBe(0);
      expect(result.pids).toBe(0);
    });

    it('rejects on Docker error', async () => {
      const mockContainer = {
        stats: jest.fn((opts, cb) => cb(new Error('container gone')))
      };

      await expect(resourceMonitor.getContainerStats(mockContainer)).rejects.toThrow('container gone');
    });
  });
});
