/**
 * Config migration tests
 *
 * These tests verify that a config file from any older version of DashCaddy
 * gets correctly migrated to the current version. Migration MUST be:
 *   - Deterministic (same input always produces same output)
 *   - Idempotent (running migration on already-migrated config is a no-op)
 *   - Safe (no data loss; only adds fields, never removes user values)
 *   - Silent (no exceptions thrown for any version from 0 to CURRENT)
 */
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
  CURRENT_VERSION,
  migrations,
  migrate,
  loadAndMigrate
} = require('../src/config/migrations');

describe('config/migrations', () => {
  let tmpDir;
  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-mig-test-'));
  });
  afterEach(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  describe('migrate()', () => {
    test('null/empty config returns fresh v_current', () => {
      const result = migrate(null);
      expect(result._version).toBe(CURRENT_VERSION);
    });

    test('undefined config returns fresh v_current', () => {
      const result = migrate(undefined);
      expect(result._version).toBe(CURRENT_VERSION);
    });

    test('v0 (no _version) migrates all the way to current', () => {
      const v0 = { tld: '.home', customValue: 'preserved' };
      const result = migrate(v0);
      expect(result._version).toBe(CURRENT_VERSION);
      // User data must be preserved
      expect(result.tld).toBe('.home');
      expect(result.customValue).toBe('preserved');
    });

    test('each intermediate version migrates forward to current', () => {
      for (let v = 0; v < CURRENT_VERSION; v++) {
        const config = { _version: v, tld: '.test' };
        const result = migrate(config);
        // Final version is always CURRENT_VERSION after running all migrations
        expect(result._version).toBe(CURRENT_VERSION);
        // User data preserved
        expect(result.tld).toBe('.test');
      }
    });

    test('config at current version passes through unchanged', () => {
      const current = { _version: CURRENT_VERSION, tld: '.home', customField: 'kept' };
      const result = migrate(current);
      expect(result).toEqual(current);
    });

    test('config from FUTURE version is left alone (forward compat)', () => {
      const future = { _version: 999, tld: '.home', newField: 'unknown' };
      const result = migrate(future);
      // We don't touch future configs — let validation catch issues
      expect(result._version).toBe(999);
      expect(result.newField).toBe('unknown');
    });
  });

  describe('v0 → v1 migration: dns normalization', () => {
    test('string dns gets converted to object', () => {
      const result = migrations[1]({ dns: '192.168.1.1' });
      expect(result.dns).toEqual({ ip: '192.168.1.1', port: 5380 });
    });

    test('missing dns gets default object', () => {
      const result = migrations[1]({ tld: '.home' });
      expect(result.dns).toEqual({ ip: '', port: 5380 });
    });

    test('object dns passes through unchanged', () => {
      const result = migrations[1]({ dns: { ip: '10.0.0.1', port: 5380, custom: 'kept' } });
      expect(result.dns.ip).toBe('10.0.0.1');
      expect(result.dns.custom).toBe('kept');
    });

    test('_version is set to 1', () => {
      const result = migrations[1]({ tld: '.home' });
      expect(result._version).toBe(1);
    });
  });

  describe('v1 → v2 migration: dns.provider field', () => {
    test('adds provider: technitium default', () => {
      const result = migrations[2]({ dns: { ip: '10.0.0.1', port: 5380 }, _version: 1 });
      expect(result.dns.provider).toBe('technitium');
      expect(result.dns.ip).toBe('10.0.0.1');
      expect(result.dns.port).toBe(5380);
    });

    test('respects existing provider if set', () => {
      const result = migrations[2]({ dns: { provider: 'cloudflare', ip: 'cf' }, _version: 1 });
      expect(result.dns.provider).toBe('cloudflare');
    });

    test('_version is set to 2', () => {
      const result = migrations[2]({ _version: 1 });
      expect(result._version).toBe(2);
    });
  });

  describe('loadAndMigrate()', () => {
    test('creates fresh config when file does not exist', () => {
      const configFile = path.join(tmpDir, 'config.json');
      const result = loadAndMigrate(configFile, null);
      expect(result._version).toBe(CURRENT_VERSION);
      // Should NOT write a file when there was nothing to migrate
      expect(fs.existsSync(configFile)).toBe(false);
    });

    test('migrates old config and writes back to disk', () => {
      const configFile = path.join(tmpDir, 'config.json');
      // Write an unversioned config (v0)
      fs.writeFileSync(configFile, JSON.stringify({ tld: '.sami', customField: 'preserve-me' }));

      const result = loadAndMigrate(configFile, null);

      // Returned value is migrated
      expect(result._version).toBe(CURRENT_VERSION);
      expect(result.tld).toBe('.sami');
      expect(result.customField).toBe('preserve-me');

      // File on disk is updated
      const written = JSON.parse(fs.readFileSync(configFile, 'utf8'));
      expect(written._version).toBe(CURRENT_VERSION);
      expect(written.tld).toBe('.sami');
    });

    test('does not rewrite file when already at current version', () => {
      const configFile = path.join(tmpDir, 'config.json');
      const original = JSON.stringify({ _version: CURRENT_VERSION, tld: '.home' }, null, 2);
      fs.writeFileSync(configFile, original);

      // Record mtime before
      const mtimeBefore = fs.statSync(configFile).mtimeMs;
      // Wait a tick
      const start = Date.now();
      while (Date.now() - start < 50) {} // 50ms busy-wait

      loadAndMigrate(configFile, null);

      // File should not have been rewritten (mtime unchanged)
      const mtimeAfter = fs.statSync(configFile).mtimeMs;
      expect(mtimeAfter).toBe(mtimeBefore);
    });

    test('handles corrupt JSON gracefully (returns defaults, no crash)', () => {
      const configFile = path.join(tmpDir, 'config.json');
      fs.writeFileSync(configFile, '{ this is not valid json');

      // Should not throw
      const result = loadAndMigrate(configFile, null);
      expect(result._version).toBe(CURRENT_VERSION);
    });

    test('creates parent directory if missing', () => {
      const nested = path.join(tmpDir, 'nested', 'subdir', 'config.json');
      // Pre-create parent dirs (test setup)
      fs.mkdirSync(path.dirname(nested), { recursive: true });
      fs.writeFileSync(nested, JSON.stringify({ tld: '.home' }));

      const result = loadAndMigrate(nested, null);
      expect(result._version).toBe(CURRENT_VERSION);
    });

    test('full chain: v0 file with string dns becomes v2 with provider', () => {
      const configFile = path.join(tmpDir, 'config.json');
      fs.writeFileSync(configFile, JSON.stringify({
        tld: '.sami',
        dns: '10.0.0.1'
      }));

      const result = loadAndMigrate(configFile, null);
      expect(result._version).toBe(CURRENT_VERSION);
      // After full chain, dns is normalized to object AND has provider
      expect(result.dns.ip).toBe('10.0.0.1');
      expect(result.dns.port).toBe(5380);
      expect(result.dns.provider).toBe('technitium');
    });
  });

  describe('idempotency', () => {
    test('running migration twice produces same result', () => {
      const v0 = { tld: '.home', customField: 'x' };
      const first = migrate(v0);
      const second = migrate(first);
      expect(second).toEqual(first);
    });

    test('loadAndMigrate is idempotent across reloads', () => {
      const configFile = path.join(tmpDir, 'config.json');
      fs.writeFileSync(configFile, JSON.stringify({ tld: '.home' }));

      const first = loadAndMigrate(configFile, null);
      const second = loadAndMigrate(configFile, null);
      expect(second).toEqual(first);
    });
  });
});
