const {
  ValidationError,
  validateDNSRecord,
  validateDockerDeployment,
  validateFilePath,
  validateVolumePath,
  validateURL,
  validateToken,
  validateServiceConfig,
  sanitizeString,
  isValidPort,
  isPrivateIP,
  validateSecurePath
} = require('../src/security/input-validator');

describe('Input Validator', () => {
  function fail(message) {
    throw new Error(message);
  }

  describe('ValidationError', () => {
    it('has correct name, message, field, and statusCode', () => {
      const err = new ValidationError('bad input', 'email');
      expect(err.name).toBe('ValidationError');
      expect(err.message).toBe('bad input');
      expect(err.field).toBe('email');
      expect(err.statusCode).toBe(400);
      expect(err).toBeInstanceOf(Error);
    });

    it('field defaults to null', () => {
      const err = new ValidationError('oops');
      expect(err.field).toBeNull();
    });
  });

  describe('validateDNSRecord', () => {
    const validRecord = { subdomain: 'myapp', ip: '8.8.8.8' };

    it('valid record returns sanitized data with lowercase subdomain and default TTL', () => {
      const result = validateDNSRecord({ subdomain: 'MyApp', ip: '1.2.3.4' });
      expect(result.subdomain).toBe('myapp');
      expect(result.ip).toBe('1.2.3.4');
      expect(result.ttl).toBe(3600);
    });

    it('accepts valid domain and custom TTL', () => {
      const result = validateDNSRecord({
        subdomain: 'test', ip: '8.8.8.8', domain: 'example.com', ttl: 300
      });
      expect(result.domain).toBe('example.com');
      expect(result.ttl).toBe(300);
    });

    it('rejects missing subdomain', () => {
      expect(() => validateDNSRecord({ ip: '1.2.3.4' })).toThrow(ValidationError);
    });

    it('rejects invalid subdomain format', () => {
      expect(() => validateDNSRecord({ subdomain: '-bad', ip: '1.2.3.4' })).toThrow(ValidationError);
      expect(() => validateDNSRecord({ subdomain: 'a'.repeat(64), ip: '1.2.3.4' })).toThrow(ValidationError);
    });

    it('rejects DNS injection chars', () => {
      const dangerous = [';', '&', '|', '`', '$', '(', ')', '<', '>', '\n', '\r', '\\'];
      for (const char of dangerous) {
        expect(() => validateDNSRecord({ subdomain: `test${char}cmd`, ip: '1.2.3.4' }))
          .toThrow(ValidationError);
      }
    });

    it('rejects invalid domain format', () => {
      expect(() => validateDNSRecord({ subdomain: 'app', ip: '1.2.3.4', domain: 'not valid!!' }))
        .toThrow(ValidationError);
    });

    it('rejects missing IP', () => {
      expect(() => validateDNSRecord({ subdomain: 'test' })).toThrow(ValidationError);
    });

    it('rejects invalid IP format', () => {
      expect(() => validateDNSRecord({ subdomain: 'test', ip: '999.999.999.999' }))
        .toThrow(ValidationError);
    });

    it('blocks private IPs when blockPrivateIPs flag set', () => {
      expect(() => validateDNSRecord({
        subdomain: 'test', ip: '192.168.1.1', blockPrivateIPs: true
      })).toThrow(ValidationError);
    });

    it('allows private IPs when flag not set', () => {
      const result = validateDNSRecord({ subdomain: 'test', ip: '192.168.1.1' });
      expect(result.ip).toBe('192.168.1.1');
    });

    it('rejects TTL below 60', () => {
      expect(() => validateDNSRecord({ subdomain: 'test', ip: '1.2.3.4', ttl: 10 }))
        .toThrow(ValidationError);
    });

    it('rejects TTL above 86400', () => {
      expect(() => validateDNSRecord({ subdomain: 'test', ip: '1.2.3.4', ttl: 100000 }))
        .toThrow(ValidationError);
    });

    it('aggregates multiple errors', () => {
      try {
        validateDNSRecord({ subdomain: '', ip: '' });
        fail('Should have thrown');
      } catch (err) {
        expect(err.errors).toBeDefined();
        expect(err.errors.length).toBeGreaterThan(1);
      }
    });
  });

  describe('validateDockerDeployment', () => {
    const valid = { name: 'my-app', image: 'nginx:latest' };

    it('valid deployment returns sanitized data', () => {
      const result = validateDockerDeployment(valid);
      expect(result.name).toBe('my-app');
      expect(result.image).toBe('nginx:latest');
      expect(result.ports).toEqual([]);
      expect(result.volumes).toEqual([]);
      expect(result.environment).toEqual({});
    });

    it('rejects missing container name', () => {
      expect(() => validateDockerDeployment({ image: 'nginx' })).toThrow(ValidationError);
    });

    it('rejects invalid container name chars', () => {
      expect(() => validateDockerDeployment({ name: '!invalid', image: 'nginx' }))
        .toThrow(ValidationError);
    });

    it('rejects container name > 255 chars', () => {
      expect(() => validateDockerDeployment({ name: 'a'.repeat(256), image: 'nginx' }))
        .toThrow(ValidationError);
    });

    it('rejects missing image', () => {
      expect(() => validateDockerDeployment({ name: 'app' })).toThrow(ValidationError);
    });

    it('blocks dangerous chars in image', () => {
      const dangerous = [';', '&', '|', '`', '$', '$(', '&&', '||', '\n'];
      for (const char of dangerous) {
        expect(() => validateDockerDeployment({ name: 'app', image: `nginx${char}rm` }))
          .toThrow(ValidationError);
      }
    });

    it('rejects image name > 512 chars', () => {
      expect(() => validateDockerDeployment({ name: 'app', image: 'a'.repeat(513) }))
        .toThrow(ValidationError);
    });

    it('validates port format "8080:80" and "8080:80/tcp"', () => {
      const result = validateDockerDeployment({
        ...valid, ports: ['8080:80', '443:443/tcp']
      });
      expect(result.ports).toEqual(['8080:80', '443:443/tcp']);
    });

    it('rejects invalid port format', () => {
      expect(() => validateDockerDeployment({ ...valid, ports: ['bad'] }))
        .toThrow(ValidationError);
    });

    it('rejects port numbers outside 1-65535', () => {
      expect(() => validateDockerDeployment({ ...valid, ports: ['99999:80'] }))
        .toThrow(ValidationError);
    });

    it('rejects ports that is not an array', () => {
      expect(() => validateDockerDeployment({ ...valid, ports: 'not-array' }))
        .toThrow(ValidationError);
    });

    it('validates volume format', () => {
      const result = validateDockerDeployment({
        ...valid, volumes: ['/data:/app/data', '/config:/app/config:ro']
      });
      expect(result.volumes).toHaveLength(2);
    });

    it('rejects volumes that is not an array', () => {
      expect(() => validateDockerDeployment({ ...valid, volumes: 'not-array' }))
        .toThrow(ValidationError);
    });

    it('validates environment variable names', () => {
      const result = validateDockerDeployment({
        ...valid, environment: { NODE_ENV: 'production', PORT: 3000, DEBUG: true }
      });
      expect(result.environment).toEqual({ NODE_ENV: 'production', PORT: 3000, DEBUG: true });
    });

    it('rejects invalid env var names', () => {
      expect(() => validateDockerDeployment({
        ...valid, environment: { '123invalid': 'val' }
      })).toThrow(ValidationError);
    });

    it('rejects environment that is not an object', () => {
      expect(() => validateDockerDeployment({ ...valid, environment: 'bad' }))
        .toThrow(ValidationError);
    });
  });

  describe('validateFilePath', () => {
    it('returns normalized path for valid input', () => {
      const result = validateFilePath('/app/data/file.json');
      expect(result).toBeDefined();
    });

    it('rejects null/empty/non-string path', () => {
      expect(() => validateFilePath(null)).toThrow(ValidationError);
      expect(() => validateFilePath('')).toThrow(ValidationError);
      expect(() => validateFilePath(123)).toThrow(ValidationError);
    });

    it('rejects directory traversal (..)', () => {
      // Use relative path so .. survives path.normalize on all platforms
      expect(() => validateFilePath('foo/../../bar')).toThrow('Path traversal detected');
    });

    it('rejects tilde (~)', () => {
      expect(() => validateFilePath('data/~/secret')).toThrow('Path traversal detected');
    });

    it('blocks sensitive paths', () => {
      if (process.platform === 'win32') {
        expect(() => validateFilePath('C:\\Windows\\System32\\config')).toThrow('not allowed');
        expect(() => validateFilePath('C:\\Program Files\\test')).toThrow('not allowed');
      } else {
        expect(() => validateFilePath('/etc/passwd')).toThrow('not allowed');
        expect(() => validateFilePath('/proc/1/status')).toThrow('not allowed');
        expect(() => validateFilePath('/sys/kernel')).toThrow('not allowed');
        expect(() => validateFilePath('/root/.ssh')).toThrow('not allowed');
        expect(() => validateFilePath('/var/run/docker.sock')).toThrow('not allowed');
        expect(() => validateFilePath('/var/lib/docker/containers')).toThrow('not allowed');
      }
    });

    it('validates against allowedBasePaths', () => {
      const result = validateFilePath('/app/data/file.txt', ['/app/data']);
      expect(result).toBeDefined();
    });

    it('rejects paths outside allowed base', () => {
      expect(() => validateFilePath('/other/file.txt', ['/app/data']))
        .toThrow('outside allowed directories');
    });
  });

  describe('validateVolumePath', () => {
    it('valid volume returns no errors', () => {
      const errors = validateVolumePath('/host/path:/container/path', 0);
      expect(errors).toHaveLength(0);
    });

    it('valid volume with mode returns no errors', () => {
      const errors = validateVolumePath('/host/path:/container/path:ro', 0);
      expect(errors).toHaveLength(0);
    });

    it('detects invalid format', () => {
      const errors = validateVolumePath('invalidformat', 0);
      expect(errors.length).toBeGreaterThan(0);
      expect(errors[0].message).toContain('Invalid volume format');
    });

    it('validates container path must be absolute', () => {
      const errors = validateVolumePath('/host:relative/path', 0);
      expect(errors.length).toBeGreaterThan(0);
    });
  });

  describe('validateURL', () => {
    it('accepts valid http/https URLs', () => {
      expect(validateURL('https://example.com')).toBe('https://example.com');
      expect(validateURL('http://example.com/path')).toBe('http://example.com/path');
    });

    it('rejects missing URL', () => {
      expect(() => validateURL(null)).toThrow(ValidationError);
      expect(() => validateURL('')).toThrow(ValidationError);
    });

    it('rejects invalid URL format', () => {
      expect(() => validateURL('not-a-url')).toThrow(ValidationError);
    });

    it('blocks private IP when blockPrivate is true', () => {
      expect(() => validateURL('http://10.0.0.1/', { blockPrivate: true }))
        .toThrow('Private URLs');
    });

    it('blocks 192.168.x.x when blockPrivate is true', () => {
      expect(() => validateURL('http://192.168.1.1/', { blockPrivate: true }))
        .toThrow('Private URLs');
    });

    it('allows private IPs when blockPrivate is false', () => {
      expect(validateURL('http://10.0.0.1/')).toBe('http://10.0.0.1/');
    });
  });

  describe('validateToken', () => {
    it('accepts valid tokens', () => {
      const result = validateToken('abcdef1234567890');
      expect(result).toBe('abcdef1234567890');
    });

    it('trims whitespace', () => {
      const result = validateToken('  validtoken  ');
      expect(result).toBe('validtoken');
    });

    it('rejects missing/non-string token', () => {
      expect(() => validateToken(null)).toThrow(ValidationError);
      expect(() => validateToken(123)).toThrow(ValidationError);
    });

    it('rejects token < 8 chars', () => {
      expect(() => validateToken('short')).toThrow('too short');
    });

    it('rejects token > 512 chars', () => {
      expect(() => validateToken('a'.repeat(513))).toThrow('too long');
    });

    it('rejects tokens with injection chars', () => {
      const dangerous = [';', '&', '|', '`', '\n', '\r', '$(', '&&'];
      for (const char of dangerous) {
        expect(() => validateToken(`validtoken${char}inject`)).toThrow('invalid characters');
      }
    });
  });

  describe('validateServiceConfig', () => {
    const valid = { id: 'my-service', name: 'My Service' };

    it('valid service config passes', () => {
      const result = validateServiceConfig(valid);
      expect(result.id).toBe('my-service');
    });

    it('rejects missing id', () => {
      expect(() => validateServiceConfig({ name: 'Test' })).toThrow(ValidationError);
    });

    it('rejects invalid id format', () => {
      expect(() => validateServiceConfig({ id: 'bad id!', name: 'Test' }))
        .toThrow(ValidationError);
    });

    it('rejects missing name', () => {
      expect(() => validateServiceConfig({ id: 'test' })).toThrow(ValidationError);
    });

    it('rejects name > 100 chars', () => {
      expect(() => validateServiceConfig({ id: 'test', name: 'x'.repeat(101) }))
        .toThrow(ValidationError);
    });

    it('validates URL when provided', () => {
      expect(() => validateServiceConfig({ id: 'test', name: 'Test', url: 'not-valid' }))
        .toThrow(ValidationError);
    });

    it('validates port when provided', () => {
      expect(() => validateServiceConfig({ id: 'test', name: 'Test', port: 99999 }))
        .toThrow(ValidationError);
    });

    it('accepts valid port', () => {
      const result = validateServiceConfig({ id: 'test', name: 'Test', port: 8080 });
      expect(result.port).toBe(8080);
    });
  });

  describe('sanitizeString', () => {
    it('escapes < > \' " to HTML entities', () => {
      expect(sanitizeString('<script>"alert(\'xss\')"</script>')).toBe(
        '&lt;script&gt;&quot;alert(&#39;xss&#39;)&quot;&lt;/script&gt;'
      );
    });

    it('truncates to maxLength', () => {
      expect(sanitizeString('hello world', 5)).toBe('hello');
    });

    it('returns empty string for non-string input', () => {
      expect(sanitizeString(123)).toBe('');
      expect(sanitizeString(null)).toBe('');
      expect(sanitizeString(undefined)).toBe('');
    });
  });

  describe('isValidPort', () => {
    it('returns true for valid ports', () => {
      expect(isValidPort(1)).toBe(true);
      expect(isValidPort(80)).toBe(true);
      expect(isValidPort(443)).toBe(true);
      expect(isValidPort(65535)).toBe(true);
    });

    it('returns false for invalid ports', () => {
      expect(isValidPort(0)).toBe(false);
      expect(isValidPort(-1)).toBe(false);
      expect(isValidPort(65536)).toBe(false);
      expect(isValidPort(NaN)).toBe(false);
    });

    it('handles string numbers', () => {
      expect(isValidPort('8080')).toBe(true);
      expect(isValidPort('0')).toBe(false);
      expect(isValidPort('abc')).toBe(false);
    });
  });

  describe('isPrivateIP', () => {
    it('identifies 10.x.x.x as private', () => {
      expect(isPrivateIP('10.0.0.1')).toBe(true);
      expect(isPrivateIP('10.255.255.255')).toBe(true);
    });

    it('identifies 172.16-31.x.x as private', () => {
      expect(isPrivateIP('172.16.0.1')).toBe(true);
      expect(isPrivateIP('172.31.255.255')).toBe(true);
    });

    it('identifies 192.168.x.x as private', () => {
      expect(isPrivateIP('192.168.1.1')).toBe(true);
    });

    it('identifies 127.x.x.x as private', () => {
      expect(isPrivateIP('127.0.0.1')).toBe(true);
    });

    it('identifies 169.254.x.x as private', () => {
      expect(isPrivateIP('169.254.0.1')).toBe(true);
    });

    it('identifies IPv6 loopback as private', () => {
      expect(isPrivateIP('::1')).toBe(true);
    });

    it('identifies fc00: and fe80: as private', () => {
      expect(isPrivateIP('fc00::1')).toBe(true);
      expect(isPrivateIP('fe80::1')).toBe(true);
    });

    it('public IPs return false', () => {
      expect(isPrivateIP('8.8.8.8')).toBe(false);
      expect(isPrivateIP('1.1.1.1')).toBe(false);
      expect(isPrivateIP('203.0.113.1')).toBe(false);
    });
  });

  describe('validateSecurePath', () => {
    const mockRealpath = jest.fn();

    beforeEach(() => {
      jest.resetModules();
      // Mock fs.promises.realpath
      jest.doMock('fs', () => ({
        ...jest.requireActual('fs'),
        promises: {
          realpath: mockRealpath,
        },
      }));
      mockRealpath.mockReset();
    });

    // Re-require after mocking fs
    function getValidateSecurePath() {
      return require('../src/security/input-validator').validateSecurePath;
    }

    it('resolves valid path within allowed roots', async () => {
      const fn = getValidateSecurePath();
      mockRealpath.mockResolvedValue('/app/data/file.txt');
      const result = await fn('/app/data/file.txt', ['/app/data']);
      expect(result).toBe('/app/data/file.txt');
    });

    it('rejects null/empty path', async () => {
      const fn = getValidateSecurePath();
      await expect(fn(null, ['/app'])).rejects.toThrow('Path is required');
      await expect(fn('', ['/app'])).rejects.toThrow('Path is required');
    });

    it('rejects null byte injection', async () => {
      const fn = getValidateSecurePath();
      await expect(fn('/app/data\0/evil', ['/app']))
        .rejects.toThrow('null byte detected');
    });

    it('rejects .. traversal sequences', async () => {
      const fn = getValidateSecurePath();
      await expect(fn('/app/../etc/passwd', ['/app']))
        .rejects.toThrow('Path traversal detected');
    });

    it('rejects URL-encoded traversal', async () => {
      const fn = getValidateSecurePath();
      await expect(fn('/app/%2e%2e/etc/passwd', ['/app']))
        .rejects.toThrow('Path traversal detected');
    });

    it('rejects path outside allowed roots', async () => {
      const fn = getValidateSecurePath();
      mockRealpath.mockResolvedValue('/other/place/file.txt');
      await expect(fn('/other/place/file.txt', ['/app/data']))
        .rejects.toThrow('outside allowed directories');
    });

    it('logs audit event when path is blocked', async () => {
      const fn = getValidateSecurePath();
      const auditLogger = { logSecurityEvent: jest.fn() };
      await expect(fn('/app/data\0evil', ['/app'], auditLogger))
        .rejects.toThrow();
      expect(auditLogger.logSecurityEvent).toHaveBeenCalledWith(
        'path_traversal_blocked',
        expect.objectContaining({ reason: 'null_byte_detected', severity: 'high' })
      );
    });

    it('handles ENOENT by checking parent', async () => {
      const fn = getValidateSecurePath();
      mockRealpath
        .mockRejectedValueOnce(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
        .mockResolvedValueOnce('/app/data'); // parent resolves
      const result = await fn('/app/data/newfile.txt', ['/app/data']);
      expect(result).toContain('newfile.txt');
    });

    it('handles EACCES with access denied error', async () => {
      const fn = getValidateSecurePath();
      mockRealpath.mockRejectedValue(Object.assign(new Error('EACCES'), { code: 'EACCES' }));
      await expect(fn('/secret/file', ['/secret']))
        .rejects.toThrow('Access denied');
    });

    it('rejects when no allowed roots configured', async () => {
      const fn = getValidateSecurePath();
      await expect(fn('/app/file', [])).rejects.toThrow('No allowed roots configured');
    });
  });
});
