/**
 * DC-048 — disk-settings-loader unit tests
 *
 * Covers:
 *  - applies persisted values to process.env (happy path)
 *  - explicit process.env wins over persisted file
 *  - missing file → no-op, no throw
 *  - malformed JSON → no throw, engine defaults preserved
 *  - non-numeric values rejected, not silently applied
 *  - empty/null/undefined values skipped
 *  - idempotent across calls (once-guard)
 *  - all six mapped keys land in env when persisted
 *
 * Run with: npx jest __tests__/disk-settings-loader.test.js
 */

'use strict';

const fs = require('fs');
const path = require('path');

// Snapshot env at module load so we can restore in afterEach. We always
// UNSET the loader-managed keys (HEALTH_*, AUDIT_*, BACKUP_*, CONTAINER_STATS_*)
// at the start of each test, regardless of whether they were set at
// snapshot time, because the loader mutates process.env and stale values
// from prior tests would silently change behavior.
const LOADER_KEYS = [
  'HEALTH_CHECK_INTERVAL', 'HEALTH_MAX_ENTRIES', 'HEALTH_HISTORY_RETENTION',
  'AUDIT_MAX_ENTRIES', 'BACKUP_MAX_STORAGE_BYTES', 'CONTAINER_STATS_MAX_ENTRIES',
];
const ORIGINAL_ENV = Object.fromEntries(
  Object.entries(process.env).filter(([k]) => LOADER_KEYS.includes(k) || k === 'DATA_DIR'),
);

function restoreEnv() {
  // Loader-managed keys: ALWAYS reset to ORIGINAL_ENV state (or undefined).
  // This is critical — without it, env vars set by a prior test would leak
  // into the next test as "env-already-set" and the loader would skip
  // values that the test expects to be applied.
  for (const k of LOADER_KEYS) {
    if (ORIGINAL_ENV[k] === undefined) {
      delete process.env[k];
    } else {
      process.env[k] = ORIGINAL_ENV[k];
    }
  }
  delete process.env.DATA_DIR;
}

// Temp data dir for filesystem-driven tests.
const TMP_DATA_DIR = '/tmp/dashcaddy-disk-settings-loader-test';
function makeDataDir() {
  try { fs.rmSync(TMP_DATA_DIR, { recursive: true, force: true }); } catch (_) { /* ok */ }
  fs.mkdirSync(TMP_DATA_DIR, { recursive: true });
}
function writePersisted(obj) {
  fs.writeFileSync(path.join(TMP_DATA_DIR, 'disk-settings.json'), JSON.stringify(obj));
}

describe('disk-settings-loader', () => {
  beforeEach(() => {
    restoreEnv();
    makeDataDir();
    // Wipe the once-guard between tests so each case sees a fresh loader run.
    // We must require the module AFTER clearing the cache.
    delete require.cache[require.resolve('../src/config/disk-settings-loader')];
    const loader = require('../src/config/disk-settings-loader');
    loader._resetForTesting();
    // Force hasRun reset (jest's module loader is not always cleared by the
    // require.cache delete — explicit call is the contract for the loader).
    // Note: loader._resetForTesting is the authoritative reset path.
  });

  afterAll(() => {
    restoreEnv();
    try { fs.rmSync(TMP_DATA_DIR, { recursive: true, force: true }); } catch (_) { /* ok */ }
  });

  it('applies all six persisted values to process.env', () => {
    writePersisted({
      healthCheckInterval: 45000,
      healthMaxEntries: 750,
      healthRetentionDays: 14,
      statsMaxEntries: 800,
      auditMaxEntries: 1500,
      backupMaxStorageBytes: 2147483648,
    });

    const loader = require('../src/config/disk-settings-loader');
    const result = loader({ dataDir: TMP_DATA_DIR });

    expect(result.applied).toHaveLength(6);
    expect(process.env.HEALTH_CHECK_INTERVAL).toBe('45000');
    expect(process.env.HEALTH_MAX_ENTRIES).toBe('750');
    expect(process.env.HEALTH_HISTORY_RETENTION).toBe('14');
    expect(process.env.CONTAINER_STATS_MAX_ENTRIES).toBe('800');
    expect(process.env.AUDIT_MAX_ENTRIES).toBe('1500');
    expect(process.env.BACKUP_MAX_STORAGE_BYTES).toBe('2147483648');
    expect(result.skipped).toEqual([]);
  });

  it('does not throw when disk-settings.json is missing', () => {
    // TMP_DATA_DIR exists but no disk-settings.json inside it.
    const loader = require('../src/config/disk-settings-loader');
    expect(() => loader({ dataDir: TMP_DATA_DIR })).not.toThrow();
    const result = loader({ dataDir: TMP_DATA_DIR }); // idempotent
    expect(result.applied).toEqual([]);
  });

  it('does not throw on malformed JSON; logs to stderr', () => {
    fs.writeFileSync(path.join(TMP_DATA_DIR, 'disk-settings.json'), '{ this is not json');
    const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);

    const loader = require('../src/config/disk-settings-loader');
    expect(() => loader({ dataDir: TMP_DATA_DIR })).not.toThrow();
    const result = loader({ dataDir: TMP_DATA_DIR });
    expect(result.applied).toEqual([]);
    expect(stderrSpy).toHaveBeenCalledWith(
      expect.stringContaining('WARN: failed to parse'),
    );
    stderrSpy.mockRestore();
  });

  it('explicit process.env wins over persisted file', () => {
    process.env.HEALTH_HISTORY_RETENTION = '90';
    writePersisted({
      healthRetentionDays: 7,
      healthMaxEntries: 999,
    });

    const loader = require('../src/config/disk-settings-loader');
    const result = loader({ dataDir: TMP_DATA_DIR });

    expect(process.env.HEALTH_HISTORY_RETENTION).toBe('90'); // unchanged
    expect(process.env.HEALTH_MAX_ENTRIES).toBe('999');       // applied
    expect(result.skipped).toEqual([
      expect.objectContaining({ envKey: 'HEALTH_HISTORY_RETENTION', reason: 'env-already-set' }),
    ]);
  });

  it('rejects non-numeric values for numeric fields', () => {
    writePersisted({
      healthCheckInterval: 'fast',  // not numeric
      healthMaxEntries: '500x',     // not numeric
      healthRetentionDays: 14,      // valid
      auditMaxEntries: null,        // silently skipped (null)
      backupMaxStorageBytes: '',    // silently skipped (empty)
    });

    const loader = require('../src/config/disk-settings-loader');
    const result = loader({ dataDir: TMP_DATA_DIR });

    // Only the valid value lands in `applied`.
    expect(result.applied.map((a) => a.envKey)).toEqual(['HEALTH_HISTORY_RETENTION']);
    // Non-numeric values appear in `skipped` with reason='non-numeric'.
    // null and '' are silently filtered (treated as "field not present").
    expect(result.skipped.map((s) => s.envKey).sort()).toEqual(
      ['HEALTH_CHECK_INTERVAL', 'HEALTH_MAX_ENTRIES'].sort(),
    );
    expect(result.skipped.every((s) => s.reason === 'non-numeric')).toBe(true);
  });

  it('coerces numeric strings (e.g. "14") to integer strings', () => {
    writePersisted({ healthRetentionDays: '14' });
    const loader = require('../src/config/disk-settings-loader');
    loader({ dataDir: TMP_DATA_DIR });
    expect(process.env.HEALTH_HISTORY_RETENTION).toBe('14');
    // Must be an integer-formatted string (not "14.7", "14x", etc.)
    expect(Number.isInteger(parseInt(process.env.HEALTH_HISTORY_RETENTION, 10))).toBe(true);
  });

  it('is idempotent across multiple calls (once-guard)', () => {
    writePersisted({ healthRetentionDays: 7 });
    const loader = require('../src/config/disk-settings-loader');
    const first = loader({ dataDir: TMP_DATA_DIR });
    const second = loader({ dataDir: TMP_DATA_DIR });
    expect(first.applied).toHaveLength(1);
    expect(second.applied).toEqual([]);
    expect(second.alreadyRun).toBe(true);
  });

  it('skips unknown fields without crashing', () => {
    writePersisted({
      healthRetentionDays: 14,
      unknownField: 'whatever',
      anotherUnknown: { nested: true },
    });
    const loader = require('../src/config/disk-settings-loader');
    expect(() => loader({ dataDir: TMP_DATA_DIR })).not.toThrow();
    expect(process.env.HEALTH_HISTORY_RETENTION).toBe('14');
  });

  it('returns a summary object with source path', () => {
    writePersisted({ healthRetentionDays: 14 });
    const loader = require('../src/config/disk-settings-loader');
    const result = loader({ dataDir: TMP_DATA_DIR });
    expect(result.source).toBe(path.join(TMP_DATA_DIR, 'disk-settings.json'));
    expect(result.alreadyRun).toBe(false);
  });

  it('writes a boot summary to stderr when no logger is provided', () => {
    writePersisted({ healthRetentionDays: 14, healthMaxEntries: 999 });
    const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);

    const loader = require('../src/config/disk-settings-loader');
    loader({ dataDir: TMP_DATA_DIR });  // no logger passed

    expect(stderrSpy).toHaveBeenCalledWith(
      expect.stringMatching(/^\[disk-settings-loader\] rehydrated 2 setting/),
    );
    stderrSpy.mockRestore();
  });
});
