import { parseRegexRule } from './parseRegexRule'

describe('parseRegexRule', () => {
  it('treats a bare pattern (no delimiters) exactly as before', () => {
    const regex = parseRegexRule('^[A-Z]{3}\\d{4}$')

    expect(regex.test('ABC1234')).toBeTrue()
    expect(regex.test('abc1234')).toBeFalse()
  })

  it('parses a delimited pattern with no flags, case-sensitively', () => {
    const regex = parseRegexRule('/\\b(the|data)\\b/')

    expect(regex.test('this is dummy data')).toBeTrue()
    expect(regex.test('THE WIND WAS BLOWING')).toBeFalse()
  })

  it('parses a delimited pattern with the i flag, case-insensitively', () => {
    const regex = parseRegexRule('/\\b(the|data)\\b/i')

    expect(regex.test('THIS IS DUMMY DATA')).toBeTrue()
    expect(regex.test('The wind was blowing')).toBeTrue()
    expect(regex.test('nothing relevant here')).toBeFalse()
  })

  it('extracts source and flags correctly from a delimited pattern', () => {
    const regex = parseRegexRule('/^[A-Z]+$/g')

    expect(regex.source).toEqual('^[A-Z]+$')
    expect(regex.flags).toEqual('g')
  })

  it('treats a bare pattern containing an internal slash as a literal, not a delimited form', () => {
    const regex = parseRegexRule('a/b')

    expect(regex.test('a/b')).toBeTrue()
    expect(regex.test('ab')).toBeFalse()
  })

  it('throws for an unterminated delimited pattern, same as a bad bare pattern would', () => {
    expect(() => parseRegexRule('/[unterminated/')).toThrow()
  })

  // parseRegexRule strips PRX's /pattern/flags delimiter wrapper and
  // translates the three PRX-only constructs below to a JS equivalent. These
  // three were picked because they're mechanical, low-risk translations with
  // one unambiguous JS equivalent. Atomic groups and possessive quantifiers are
  // NOT translated - both would need a lookahead+backreference rewrite that
  // changes the pattern's capture-group numbering, which is a real
  // correctness risk for comparatively rare constructs, so they're left as a
  // documented limitation below instead.
  describe('translated PRX-only syntax', () => {
    it('hoists a leading (?i) inline modifier into the i flag', () => {
      const regex = parseRegexRule('/(?i)abc/')

      expect(regex.test('ABC')).toBeTrue()
      expect(regex.test('abc')).toBeTrue()
    })

    it('merges a leading (?i) with flags already present after the closing delimiter', () => {
      const regex = parseRegexRule('/(?i)^[a-z]+$/g')

      expect(regex.flags).toContain('i')
      expect(regex.flags).toContain('g')
      expect(regex.test('ABC')).toBeTrue()
    })

    it('does not add a duplicate i flag when (?i) and the trailing /i are both present', () => {
      expect(() => parseRegexRule('/(?i)abc/i')).not.toThrow()
    })

    it('treats \\Q...\\E as a literal, metacharacter-escaped sequence', () => {
      const regex = parseRegexRule('/\\Qa+b*c\\E/')

      expect(regex.test('a+b*c')).toBeTrue()
      // Without the \Q...\E escaping, '+' and '*' would be quantifiers, so
      // this would also match a literal "c" alone. It shouldn't.
      expect(regex.test('c')).toBeFalse()
    })

    it('translates \\A/\\z to absolute start/end anchors equivalent to PRX semantics', () => {
      const regex = parseRegexRule('/\\Afoo\\z/')

      expect(regex.test('foo')).toBeTrue()
      expect(regex.test('foobar')).toBeFalse()
      expect(regex.test('barfoo')).toBeFalse()
      // \z (unlike JS's own $) does not match immediately before a
      // trailing newline - this is stricter, matching PRX/Perl semantics.
      expect(regex.test('foo\n')).toBeFalse()
    })
  })

  // Pre-existing gaps, not regressions from this fix - both fail safely:
  // they throw, same as any other malformed pattern, and each call site
  // already treats a throw as "always valid" / "never warn".
  describe('known limitations - PRX-only syntax not translated', () => {
    it('throws on free-spacing/extended (/x) mode (valid PRX, not a JS flag)', () => {
      expect(() => parseRegexRule('/a+ \\s+ b/x')).toThrow()
    })

    it('throws on an atomic group (?>...) (valid PRX, unsupported in JS)', () => {
      expect(() => parseRegexRule('/(?>a|ab)c/')).toThrow()
    })

    it('throws on a possessive quantifier, e.g. a++ (valid PRX, unsupported in JS)', () => {
      expect(() => parseRegexRule('/a++b/')).toThrow()
    })
  })

  // HARDREGEX/SOFTREGEX RULE_VALUEs collected from an existing
  // MPE_VALIDATIONS table. None of these use PRX-only syntax (no inline
  // (?i), \Q...\E, \A/\z, atomic groups, or possessive quantifiers), so
  // they pass through parseRegexRule with only the /pattern/flags
  // delimiter stripped - the same PCRE-compatible escape rules apply on
  // both the SAS PRX side and the JS side, so behavior here is verified
  // to match, not just "doesn't throw".
  describe('RULE_VALUE corpus collected from an existing MPE_VALIDATIONS table', () => {
    const collectedRuleValues = [
      '/^-?[0-9]{1,13}([.]{0,1}[0-9]{0,3})$/',
      '/^-?[0-9]{1,3}$/',
      '/^(?:(?!31\\\\.12).)*$/',
      '/^([0-9]{1,3}([.][0-9]{1,7})?)%$/',
      '/^(100|[0-9]{1,2})%$/',
      '/^[-,0-9]{1,13}([.]{0,1}[0-9]{0,3})$/',
      '/^[-0-9]{1,19}([.]{0,1}[0-9]{0,3})$/',
      '/^[-0-9]*([.]{0,1}[0-9]{0,2})$/',
      '/^[-0-9]*([.]{0,1}[0-9]{0,3})$/',
      '/^[-0-9]*([.]{0,1}[0-9]*)$/',
      '/^[-0-9]*$/',
      '/^[0-2]$/',
      '/^[0-3]$/',
      '/^[0-9]{1,19}([.]{0,1}[0-9]{0,3})$/',
      '/^[0-9]{1,36}$/',
      '/^[0-9]{1,72}$/',
      '/^[0-9]{4}$/',
      '/^[0-9]{5}$/',
      '/^[0-9]*([.]{0,1}[0-9]{0,4})$/',
      '/^[0,1]{1}([.]{0,1}[0-9]{0,4})$/',
      '/^[1]{1,1}$/',
      '/^[A-Z0-1]{5,5}$/',
      '/^[a-zA-Z]{1,1}$/',
      '/^[a-zA-Z]{1,3}$/',
      '/^[a-zA-Z0-9_]{0,36}$/',
      '/^[a-zA-Z0-9_=@\\/.:;,= -]{1,8}$/',
      '/^[a-zA-Z0-9@_-]{1,100}$/',
      '/^[a-zA-Z0-9@_-]{1,50}$/',
      '/^[a-zA-Z0-9=@.:;,= -_]{1,50}$/',
      '/^[a-zA-Z0-9=@.:;,= -_]{1,80}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,10}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,100}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,12}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,14}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,15}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,20}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,3}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,300}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,32}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,4}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,40}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,400}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,50}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= \\-_]{1,500}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,510}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,6}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,72}$/',
      '/^[a-zA-Z0-9=@\\/.:;,= -_]{1,8}$/',
      "/^[a-zA-Z0-9=@\\/.:;,= -_`']{1,100}$/",
      "/^[a-zA-Z0-9=@\\/.:;,= -_`']{1,20}$/",
      "/^[a-zA-Z0-9=@\\/.:;,= \\-_`']{1,500}$/",
      "/^[a-zA-Z0-9=@\\/.:;,= -_`']*$/"
    ]

    collectedRuleValues.forEach((ruleValue) => {
      it(`compiles without throwing: ${ruleValue}`, () => {
        expect(() => parseRegexRule(ruleValue)).not.toThrow()
      })
    })

    it('preserves the exact pattern body and applies no flags for a plain numeric-range rule', () => {
      const regex = parseRegexRule('/^[0-9]{1,13}([.]{0,1}[0-9]{0,3})$/')

      expect(regex.source).toEqual('^[0-9]{1,13}([.]{0,1}[0-9]{0,3})$')
      expect(regex.flags).toEqual('')
      expect(regex.test('1234.56')).toBeTrue()
      expect(regex.test('abc')).toBeFalse()
    })

    it('handles the percentage-with-alternation rule (100 or 0-99, optionally with a decimal)', () => {
      const regex = parseRegexRule('/^(100|[0-9]{1,2})%$/')

      expect(regex.test('100%')).toBeTrue()
      expect(regex.test('42%')).toBeTrue()
      expect(regex.test('101%')).toBeFalse()
    })

    it('handles a character class with an escaped delimiter slash and a non-edge escaped hyphen', () => {
      const regex = parseRegexRule('/^[a-zA-Z0-9=@\\/.:;,= \\-_]{1,500}$/')

      expect(regex.test('a/b-c d.e:f;g,h=i')).toBeTrue()
      expect(regex.test('has a # in it')).toBeFalse()
    })

    it('handles a character class with literal backtick/single-quote characters', () => {
      const regex = parseRegexRule("/^[a-zA-Z0-9=@\\/.:;,= -_`']*$/")

      expect(regex.test("O'Brien`s value")).toBeTrue()
      // The unescaped space-to-underscore run (` -_`) in this class is a
      // range (space is code point 32, underscore 95), not three literal
      // characters - so it also matches '#' (35), which falls inside that
      // range. Same behavior in SAS PRX (identical character-class rules),
      // so this is a faithful translation, not a parseRegexRule bug.
      expect(regex.test('has a # in it')).toBeTrue()
      expect(regex.test('has a ~ in it')).toBeFalse()
    })

    // This is the one inconsistency in the whole corpus: every other escaped
    // '/' in this file uses a single backslash, but this rule has \\. (two
    // backslashes) where every other sign points to a single \. having been
    // intended (a literal dot, inside a negative lookahead meant to exclude
    // "31.12" - e.g. a 31-December date - from matching). SAS PRX and JS
    // regex use identical backslash-escape semantics for this (no PRX-only
    // construct is involved), so parseRegexRule's output is a faithful,
    // parity-preserving translation of whatever was actually authored -
    // this test pins down what the corpus *actually* does today, verbatim,
    // not what was likely intended. \\ (escaped backslash) followed by an
    // unescaped . (any character) means the lookahead is functionally a
    // no-op: it only fails to match text containing a literal backslash,
    // which excludes nothing in practice, so every value - including dates
    // that literally contain "31.12" - passes.
    it('as authored (double backslash): the 31.12-exclusion lookahead is a no-op and matches everything', () => {
      const regex = parseRegexRule('/^(?:(?!31\\\\.12).)*$/')

      expect(regex.test('31.12.2024')).toBeTrue()
      expect(regex.test('01.01.2024')).toBeTrue()
    })
  })
})
