import { expect } from 'chai';
import {
  escapeDnValue,
  unescapeDnValue,
  escapeLdapFilter,
  validateDnValue,
  parseDn,
  getParentDn,
  getRdn,
  isChildOf,
  normalizeDn,
  isDnInBranch,
} from '../../src/lib/utils';

describe('LDAP Utils', () => {
  describe('escapeDnValue', () => {
    it('should escape comma in DN values', () => {
      expect(escapeDnValue('Smith, John')).to.equal('Smith\\, John');
    });

    it('should escape plus sign in DN values', () => {
      expect(escapeDnValue('user+admin')).to.equal('user\\+admin');
    });

    it('should escape backslash in DN values', () => {
      expect(escapeDnValue('path\\to\\file')).to.equal('path\\\\to\\\\file');
    });

    it('should escape multiple special characters', () => {
      expect(escapeDnValue('a,b+c=d')).to.equal('a\\,b\\+c\\=d');
    });

    it('should escape leading space', () => {
      expect(escapeDnValue(' leadingspace')).to.equal('\\ leadingspace');
    });

    it('should escape trailing space', () => {
      expect(escapeDnValue('trailingspace ')).to.equal('trailingspace\\ ');
    });

    it('should escape leading hash', () => {
      expect(escapeDnValue('#comment')).to.equal('\\#comment');
    });

    it('should escape quotes and angle brackets', () => {
      expect(escapeDnValue('"test"')).to.equal('\\"test\\"');
      expect(escapeDnValue('<tag>')).to.equal('\\<tag\\>');
    });

    it('should handle normal strings without escaping', () => {
      expect(escapeDnValue('normaluser123')).to.equal('normaluser123');
    });
  });

  describe('unescapeDnValue', () => {
    it('should unescape comma in DN values', () => {
      expect(unescapeDnValue('Smith\\, John')).to.equal('Smith, John');
    });

    it('should unescape plus sign in DN values', () => {
      expect(unescapeDnValue('user\\+admin')).to.equal('user+admin');
    });

    it('should unescape backslash in DN values', () => {
      expect(unescapeDnValue('path\\\\to\\\\file')).to.equal('path\\to\\file');
    });

    it('should unescape multiple special characters', () => {
      expect(unescapeDnValue('a\\,b\\+c\\=d')).to.equal('a,b+c=d');
    });

    it('should unescape leading space', () => {
      expect(unescapeDnValue('\\ leadingspace')).to.equal(' leadingspace');
    });

    it('should unescape trailing space', () => {
      expect(unescapeDnValue('trailingspace\\ ')).to.equal('trailingspace ');
    });

    it('should unescape leading hash', () => {
      expect(unescapeDnValue('\\#comment')).to.equal('#comment');
    });

    it('should unescape quotes and angle brackets', () => {
      expect(unescapeDnValue('\\"test\\"')).to.equal('"test"');
      expect(unescapeDnValue('\\<tag\\>')).to.equal('<tag>');
    });

    it('should handle hex-encoded characters', () => {
      expect(unescapeDnValue('user\\00name')).to.equal('user\0name');
      expect(unescapeDnValue('path\\5cname')).to.equal('path\\name');
    });

    it('should handle normal strings without changes', () => {
      expect(unescapeDnValue('normaluser123')).to.equal('normaluser123');
    });

    it('should be the inverse of escapeDnValue', () => {
      const original = 'Smith, John+Admin';
      expect(unescapeDnValue(escapeDnValue(original))).to.equal(original);
    });

    it('should roundtrip complex values', () => {
      const values = [
        'user+admin',
        'Smith, John',
        'path\\to\\file',
        '"quoted"',
        '<tag>',
        '#comment',
        ' leadingspace',
        'trailingspace ',
        'a,b+c=d<e>f;g"h',
      ];
      for (const val of values) {
        expect(unescapeDnValue(escapeDnValue(val))).to.equal(val);
      }
    });
  });

  describe('escapeLdapFilter', () => {
    it('should escape asterisk in filter values', () => {
      expect(escapeLdapFilter('user*')).to.equal('user\\2a');
    });

    it('should escape parentheses in filter values', () => {
      expect(escapeLdapFilter('(admin)')).to.equal('\\28admin\\29');
    });

    it('should escape backslash in filter values', () => {
      expect(escapeLdapFilter('path\\name')).to.equal('path\\5cname');
    });

    it('should handle complex filter injection attempts', () => {
      // Attempt to inject: )(uid=*)
      const malicious = ')(uid=*)';
      const escaped = escapeLdapFilter(malicious);
      expect(escaped).to.equal('\\29\\28uid=\\2a\\29');
    });
  });

  describe('validateDnValue', () => {
    it('should accept valid alphanumeric values', () => {
      expect(() => validateDnValue('user123', 'uid')).to.not.throw();
    });

    it('should accept values with allowed special characters', () => {
      expect(() =>
        validateDnValue('john.doe@example.com', 'mail')
      ).to.not.throw();
      expect(() => validateDnValue('Smith, John', 'cn')).to.not.throw();
      expect(() => validateDnValue("O'Brien", 'sn')).to.not.throw();
    });

    it('should reject null character', () => {
      expect(() => validateDnValue('user\x00name', 'uid')).to.throw(
        'uid contains invalid control characters'
      );
    });

    it('should reject newline characters', () => {
      expect(() => validateDnValue('user\nname', 'uid')).to.throw(
        'uid contains invalid control characters'
      );
      expect(() => validateDnValue('user\rname', 'uid')).to.throw(
        'uid contains invalid control characters'
      );
    });

    it('should reject tab character', () => {
      expect(() => validateDnValue('user\tname', 'uid')).to.throw(
        'uid contains invalid control characters'
      );
    });

    it('should reject zero-width space', () => {
      expect(() => validateDnValue('user\u200Bname', 'uid')).to.throw(
        'uid contains invalid invisible characters'
      );
    });

    it('should reject BOM character', () => {
      expect(() => validateDnValue('\uFEFFuser', 'uid')).to.throw(
        'uid contains invalid invisible characters'
      );
    });

    it('should reject empty string', () => {
      expect(() => validateDnValue('', 'uid')).to.throw(
        'uid must be a non-empty string'
      );
    });

    it('should reject whitespace-only string', () => {
      expect(() => validateDnValue('   ', 'uid')).to.throw(
        'uid must be a non-empty string'
      );
    });

    it('should include field name in error message', () => {
      expect(() =>
        validateDnValue('bad\x00value', 'organizationalUnit')
      ).to.throw('organizationalUnit contains invalid control characters');
    });
  });

  describe('parseDn', () => {
    it('should parse simple DN', () => {
      const parts = parseDn('uid=user,ou=users,dc=example,dc=com');
      expect(parts).to.deep.equal([
        'uid=user',
        'ou=users',
        'dc=example',
        'dc=com',
      ]);
    });

    it('should handle escaped commas in DN', () => {
      const parts = parseDn('cn=Smith\\, John,ou=users,dc=example,dc=com');
      expect(parts).to.deep.equal([
        'cn=Smith\\, John',
        'ou=users',
        'dc=example',
        'dc=com',
      ]);
    });
  });

  describe('getParentDn', () => {
    it('should return parent DN', () => {
      expect(getParentDn('uid=user,ou=users,dc=example,dc=com')).to.equal(
        'ou=users,dc=example,dc=com'
      );
    });

    it('should return same DN if no parent', () => {
      expect(getParentDn('dc=com')).to.equal('dc=com');
    });
  });

  describe('getRdn', () => {
    it('should return first RDN component', () => {
      expect(getRdn('uid=user,ou=users,dc=example,dc=com')).to.equal(
        'uid=user'
      );
    });
  });

  describe('isChildOf', () => {
    it('should return true for direct child', () => {
      expect(
        isChildOf(
          'uid=user,ou=users,dc=example,dc=com',
          'ou=users,dc=example,dc=com'
        )
      ).to.be.true;
    });

    it('should return false for same DN', () => {
      expect(
        isChildOf('ou=users,dc=example,dc=com', 'ou=users,dc=example,dc=com')
      ).to.be.false;
    });

    it('should return false for unrelated DNs', () => {
      expect(
        isChildOf(
          'uid=user,ou=users,dc=example,dc=com',
          'ou=groups,dc=example,dc=com'
        )
      ).to.be.false;
    });
  });

  describe('normalizeDn', () => {
    it('should lowercase and drop whitespace around separators', () => {
      expect(normalizeDn('UID=x, ou=AppAccounts ,dc=Example,dc=com')).to.equal(
        'uid=x,ou=appaccounts,dc=example,dc=com'
      );
    });

    it('should collapse whitespace around the = of an RDN', () => {
      expect(normalizeDn('ou = AppAccounts , dc = example')).to.equal(
        'ou=appaccounts,dc=example'
      );
    });

    it('should order multi-valued RDN assertions deterministically', () => {
      expect(normalizeDn('ou=b+cn=a,dc=example')).to.equal(
        normalizeDn('cn=a+ou=b,dc=example')
      );
    });

    it('should preserve escaped commas as a single RDN', () => {
      expect(normalizeDn('cn=Smith\\, John,ou=users')).to.equal(
        'cn=smith\\, john,ou=users'
      );
    });
  });

  describe('isDnInBranch', () => {
    it('should return true for a descendant', () => {
      expect(
        isDnInBranch(
          'uid=alice_c1,ou=appAccounts,dc=example,dc=com',
          'ou=appAccounts,dc=example,dc=com'
        )
      ).to.be.true;
    });

    it('should return true for the base itself', () => {
      expect(
        isDnInBranch(
          'ou=appAccounts,dc=example,dc=com',
          'ou=appAccounts,dc=example,dc=com'
        )
      ).to.be.true;
    });

    it('should be insensitive to case and whitespace (the cascade-guard hardening)', () => {
      expect(
        isDnInBranch(
          'uid=alice_c1, ou=AppAccounts, dc=example, dc=com',
          'ou=appaccounts,dc=example,dc=com'
        )
      ).to.be.true;
    });

    it('should tolerate a trailing newline/space in the configured base', () => {
      expect(
        isDnInBranch(
          'uid=alice_c1,ou=appAccounts,dc=example,dc=com',
          'ou=appAccounts,dc=example,dc=com\n'
        )
      ).to.be.true;
    });

    it('should return false for a different branch', () => {
      expect(
        isDnInBranch(
          'uid=user,ou=users,dc=example,dc=com',
          'ou=appAccounts,dc=example,dc=com'
        )
      ).to.be.false;
    });

    it('should not match on a non-aligned suffix', () => {
      // "accounts,dc=example,dc=com" is a raw-string suffix but NOT an RDN-aligned one
      expect(
        isDnInBranch(
          'uid=x,ou=otheraccounts,dc=example,dc=com',
          'ou=accounts,dc=example,dc=com'
        )
      ).to.be.false;
    });

    it('should return false when base is empty', () => {
      expect(isDnInBranch('uid=x,ou=a,dc=c', '')).to.be.false;
    });
  });
});
