import { test } from '@japa/runner'
import sinon from 'sinon'
import { clear } from '../../../../src/runtime/variables/Cache.js'
import { VariablesProcessor, Property } from '../../../../src/index.js'
import { EvalFunctions } from '../../../../src/runtime/variables/EvalFunctions.js'

const variables: Property[] = [
  Property.String('test1', 'value1'),
  Property.String('test2', 'value2 ${test1}'),
  Property.String('test3', 'value3 {test4}'),
  Property.String('test4', 'value4'),
  Property.String('test5', 'value5', false),
  Property.String('host', 'api'),
  Property.String('path', 'path'),
  Property.String('b46', 'other'),
]

test.group('Variables processing', (group) => {
  let instance: VariablesProcessor
  group.each.setup(() => {
    instance = new VariablesProcessor()
    return (): void => clear(instance)
  })

  test('{$i} evaluates variable')
    .with([
      ['Test 1: ${test1}', 'Test 1: value1'],
      ['Test 2: {test1}', 'Test 2: value1'],
      ['Test 3: ${test2}', 'Test 3: value2 value1'],
      ['Test 4: {test2}', 'Test 4: value2 value1'],
      ['Test 5: ${test3}', 'Test 5: value3 value4'],
      ['Test 6: {test3}', 'Test 6: value3 value4'],
      ['Test 7: ${test5}', 'Test 7: undefined'],
      ['Test 8: {test5}', 'Test 8: undefined'],
      ['${String.toLowerCase(TEST)}', 'test'],
      ['{String.toLowerCase(TEST)}', 'test'],
      ['${Math.abs(-110)}', '110'],
      ['{Math.abs(-110)}', '110'],
      ['${encodeURIComponent(te s+t)}', 'te%20s%2Bt'],
      ['{encodeURIComponent(te s+t)}', 'te%20s%2Bt'],
      ['${decodeURIComponent(te%20s%2Bt)}', 'te s+t'],
      ['{decodeURIComponent(te%20s%2Bt)}', 'te s+t'],
      ['{btoa(test)}', 'dGVzdA=='],
      ['{atob(dGVzdA==)}', 'test'],
      ['{\n\t"v1":"${test1}",\n\t"v2": "${test2}"\n}', '{\n\t"v1":"value1",\n\t"v2": "value2 value1"\n}'],
      ['{"v1":"${test1}","v2": "${test2}"}', '{"v1":"value1","v2": "value2 value1"}'],
      ['{\n\t"v1":"{test1}",\n\t"v2": "{test2}"\n}', '{\n\t"v1":"value1",\n\t"v2": "value2 value1"\n}'],
      ['{"v1":"{test1}","v2": "{test2}"}', '{"v1":"value1","v2": "value2 value1"}'],
      ['https://{host}.domain.com', 'https://api.domain.com'],
      ['https://api.domain.com/a/{path}/b', 'https://api.domain.com/a/path/b'],
      [JSON.stringify({ data: { complex: true } }, null, 2), '{\n  "data": {\n    "complex": true\n  }\n}'],
      ['{"test":true}', '{"test":true}'],
      ['[{"test":true}]', '[{"test":true}]'],
    ])
    .run(async ({ assert }, [src, value]) => {
      const ctx = VariablesProcessor.createContextFromProperties(variables)
      const result = await instance.evaluateVariable(src, ctx)
      assert.equal(result, value)
    })

  test('{$i} evaluates variables with regexp')
    .with([
      ['JS syntax: ${now}', /JS syntax: \d+/],
      ['API syntax: {now}', /API syntax: \d+/],
      ['JS syntax: ${random()}', /JS syntax: \d+/],
      ['API syntax: {random()}', /API syntax: \d+/],
    ])
    .run(async ({ assert }, [src, value]) => {
      const ctx = VariablesProcessor.createContextFromProperties(variables)
      const result = await instance.evaluateVariable(src as string, ctx)
      assert.match(result, value as RegExp)
    })
})

test.group('_applyArgumentsContext()', (group) => {
  let instance: VariablesProcessor
  group.each.setup(() => {
    instance = new VariablesProcessor()
  })

  test('returns the same string if not a variable', ({ assert }) => {
    const result = instance._applyArgumentsContext('test', {})
    assert.equal(result, 'test')
  })

  test('replaces value with context value (JS syntax)', ({ assert }) => {
    const result = instance._applyArgumentsContext('${test}', {
      test: 'other',
    })
    assert.equal(result, 'other')
  })

  test('replaces value with context value (API syntax)', ({ assert }) => {
    const result = instance._applyArgumentsContext('{test}', {
      test: 'other',
    })
    assert.equal(result, 'other')
  })

  test('returns expression value if no key in context (JS syntax)', ({ assert }) => {
    const result = instance._applyArgumentsContext('${test}', {})
    assert.equal(result, '${test}')
  })

  test('returns expression value if no key in context (API syntax)', ({ assert }) => {
    const result = instance._applyArgumentsContext('{test}', {})
    assert.equal(result, '{test}')
  })
})

test.group('evaluateVariables()', (group) => {
  const vars: Property[] = [
    Property.String('test1', 'value1'),
    Property.String('test2', 'value2 ${test1}'),
    Property.String('test3', 'value3 ${test4}'),
    Property.String('test4', 'value4'),
    Property.String('test5', 'value5', false),
  ]
  const obj = {
    var1: '${test1}',
    var2: '${test2}',
    var3: 'test-${test4}',
    var4: 'hello',
  }

  let instance: VariablesProcessor
  group.each.setup(() => {
    instance = new VariablesProcessor()
  })

  test('returns the same string without variables', async ({ assert }) => {
    const tmp = { ...obj }
    const result = await instance.evaluateVariables(tmp, {})
    assert.equal(result.var4, 'hello')
  })

  test('evaluates only listed properties', async ({ assert }) => {
    const tmp = { ...obj }
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariables(tmp, ctx, ['var1'])
    assert.equal(result.var1, 'value1')
    assert.equal(result.var2, '${test2}')
    assert.equal(result.var3, 'test-${test4}')
    assert.equal(result.var4, 'hello')
  })

  test('evaluates all properties', async ({ assert }) => {
    const tmp = { ...obj }
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariables(tmp, ctx)
    assert.equal(result.var1, 'value1')
    assert.equal(result.var2, 'value2 value1')
    assert.equal(result.var3, 'test-value4')
    assert.equal(result.var4, 'hello')
  })
})

test.group('evaluateVariable()', (group) => {
  const vars: Property[] = [
    Property.String('test1', 'value1'),
    Property.String('test2', 'value2 ${test1}'),
    Property.String('test3', 'value3 ${test4}'),
    Property.String('test4', 'value4'),
    Property.String('test5', 'value5', false),
  ]

  let instance: VariablesProcessor
  group.each.setup(() => {
    instance = new VariablesProcessor()
  })

  test('returns the same string without variables', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test', ctx)
    assert.equal(result, 'test')
  })

  test('returns value for variable', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test ${test1}', ctx)
    assert.equal(result, 'test value1')
  })

  test('evaluates JSON string', async ({ assert }) => {
    const str = '{\n\t"v1":"${test1}",\n\t"v2": "${test2}"\n}'
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable(str, ctx)
    assert.equal(result, '{\n\t"v1":"value1",\n\t"v2": "value2 value1"\n}')
  })

  test('returns value for complex variable', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test ${test3}', ctx)
    assert.equal(result, 'test value3 value4')
  })

  test('evaluates legacy now() function', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test ${now}', ctx)
    const now = result.split(' ')[1]
    assert.isFalse(Number.isNaN(now))
  })

  test('evaluates legacy now() function with a group', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('${now:1} ${now:2} ${now:1}', ctx)
    const values = result.split(' ')
    assert.isFalse(Number.isNaN(values[0]))
    assert.equal(values[0], values[2])
  })

  test('evaluates legacy random() function', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test ${random}', ctx)
    const value = result.split(' ')[1]
    assert.isFalse(Number.isNaN(value))
  })

  test('evaluates legacy random() function with a group', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('${random:1} ${random:2} ${random:1}', ctx)
    const values = result.split(' ')
    assert.isFalse(Number.isNaN(values[0]))
    assert.equal(values[0], values[2])
    assert.notEqual(values[1], values[2])
  })

  test('evaluates now()', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test now()', ctx)
    const now = result.split(' ')[1]
    assert.isFalse(Number.isNaN(now))
  })

  test('evaluates now() with a group', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('now(1) now(2) now(1)', ctx)
    const values = result.split(' ')
    assert.equal(values[0], values[2])
  })

  test('evaluates random()', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test random()', ctx)
    const now = result.split(' ')[1]
    assert.isFalse(Number.isNaN(now))
  })

  test('evaluates random() with group', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('random(1) random(2) random(1)', ctx)
    const values = result.split(' ')
    assert.equal(values[0], values[2])
  })

  test('evaluates Math function', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test Math.abs(-100)', ctx)
    assert.equal(result, 'test 100')
  })

  test('evaluates String function', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test String.toUpperCase(test)', ctx)
    assert.equal(result, 'test TEST')
  })

  test('evaluates encodeURIComponent()', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test encodeURIComponent(te s+t)', ctx)
    assert.equal(result, 'test te%20s%2Bt')
  })

  test('evaluates decodeURIComponent()', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test decodeURIComponent(te%20s%2Bt)', ctx)
    assert.equal(result, 'test te s+t')
  })

  test('ignores invalid input', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('test ${test', ctx)
    assert.equal(result, 'test ${test')
  })

  test('does not evaluate object', async ({ assert }) => {
    const input = { a: 'b' }
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    // @ts-expect-error Used in testing
    const result = await instance.evaluateVariable(input, ctx)
    assert.isTrue((input as unknown) === result)
  })

  test('does not evaluate null', async ({ assert }) => {
    const input = null
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    // @ts-expect-error Used in testing
    const result = await instance.evaluateVariable(input, ctx)
    assert.isTrue(input === result)
  })

  test('does evaluate numbers', async ({ assert }) => {
    const input = 2
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    // @ts-expect-error Used in testing
    const result = await instance.evaluateVariable(input, ctx)
    assert.isTrue(result === '2')
  })

  test('does not evaluate booleans', async ({ assert }) => {
    const input = false
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    // @ts-expect-error Used in testing
    const result = await instance.evaluateVariable(input, ctx)
    assert.isTrue(result === 'false')
  })

  test('preserves the double slash', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(vars)
    const result = await instance.evaluateVariable('\\\\test\\\\', ctx)
    assert.equal(result, '\\\\test\\\\')
  })
})

test.group('_prepareValue()', (group) => {
  let instance: VariablesProcessor
  group.each.setup(() => {
    instance = new VariablesProcessor()
  })

  test('Prepares simple string', ({ assert }) => {
    assert.equal(instance._prepareValue('test'), 'test')
  })

  test('Prepares string with variable', ({ assert }) => {
    assert.equal(instance._prepareValue('test ${val}'), "'test ' + val + ''")
  })

  test('does not throw error for bad syntax', ({ assert }) => {
    assert.doesNotThrow(() => {
      instance._prepareValue('test ${val')
    })
  })

  test('Prepares string with complex structure', ({ assert }) => {
    const result = instance._prepareValue('test ${val} test ${val} test ${val}')
    const compare = "'test ' + val + ' test ' + val + ' test ' + val + ''"
    assert.equal(result, compare)
  })

  test('prepares API literal syntax', ({ assert }) => {
    const result = instance._prepareValue('test {val} test {val} test {val}')
    const compare = "'test ' + val + ' test ' + val + ' test ' + val + ''"
    assert.equal(result, compare)
  })
})

test.group('_upgradeLegacy()', (group) => {
  let instance: VariablesProcessor
  group.each.setup(() => {
    instance = new VariablesProcessor()
  })

  test('upgrades ${now}', ({ assert }) => {
    assert.equal(instance._upgradeLegacy('test ${now}'), 'test ${now()}')
  })

  test('upgrades ${now} with groups', ({ assert }) => {
    assert.equal(instance._upgradeLegacy('test ${now:1}'), 'test ${now(1)}')
  })

  test('upgrades ${random}', ({ assert }) => {
    assert.equal(instance._upgradeLegacy('test ${random}'), 'test ${random()}')
  })

  test('upgrades ${random} with groups', ({ assert }) => {
    assert.equal(instance._upgradeLegacy('test ${random:1}'), 'test ${random(1)}')
  })
})

test.group('buildContext()', (group) => {
  let instance: VariablesProcessor
  group.each.setup(() => {
    instance = new VariablesProcessor()
  })

  test('sets variable value', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(variables)
    const context = await instance.buildContext(ctx)
    assert.equal(context.test2, 'value2 value1')
  })

  test('sets variable value defined later', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(variables)
    const context = await instance.buildContext(ctx)
    assert.equal(context.test3, 'value3 value4')
  })

  test('does not uses disabled items', async ({ assert }) => {
    const ctx = VariablesProcessor.createContextFromProperties(variables)
    const context = await instance.buildContext(ctx)
    assert.isUndefined(context.test5)
  })
})

test.group('_callNamespaceFunction()', (group) => {
  let instance: VariablesProcessor
  let ctx: Record<string, string>
  group.each.setup(async () => {
    instance = new VariablesProcessor()
    const vars = VariablesProcessor.createContextFromProperties(variables)
    ctx = await instance.buildContext(vars)
  })

  test('returns empty string when namespace does not exist', ({ assert }) => {
    const result = instance._callNamespaceFunction(ctx, 'Something', 'fn', [])
    assert.equal(result, '')
  })

  test('Calls Math function', ({ assert }) => {
    const result = instance._callNamespaceFunction(ctx, 'Math', 'abs', ['1'])
    assert.equal(result, 1)
  })

  test('Calls JSON function', ({ assert }) => {
    const result = instance._callNamespaceFunction(ctx, 'JSON', 'parse', ['{}'])
    assert.deepEqual(result, {})
  })

  test('Calls String function', ({ assert }) => {
    const result = instance._callNamespaceFunction(ctx, 'String', 'substr', ['test', '1'])
    assert.equal(result, 'est')
  })

  test('Throws when String function has no arguments', ({ assert }) => {
    assert.throws(() => {
      instance._callNamespaceFunction(ctx, 'String', 'substr')
    })
  })
})

test.group('_evalFunctions()', (group) => {
  let instance: VariablesProcessor
  let ctx: Record<string, string>
  group.setup(async () => {
    instance = new VariablesProcessor()
    const vars = VariablesProcessor.createContextFromProperties(variables)
    ctx = await instance.buildContext(vars)
  })

  test('returns empty string when no argument', ({ assert }) => {
    // @ts-expect-error Used in testing
    const result = instance._evalFunctions(undefined, ctx)
    assert.equal(result, '')
  })

  test('calls the now()', ({ assert }) => {
    const result = instance._evalFunctions('now()', ctx)
    assert.isFalse(Number.isNaN(result))
  })

  test('calls the random()', ({ assert }) => {
    const result = instance._evalFunctions('random()', ctx)
    assert.isFalse(Number.isNaN(result))
  })

  test('calls the random() with groups', ({ assert }) => {
    const result = instance._evalFunctions('random(1) random(2) random(1)', ctx)
    const items = result.split(' ')
    assert.equal(items[0], items[2])
  })

  test('calls the Math function', ({ assert }) => {
    const result = instance._evalFunctions('test Math.abs(-110)', ctx)
    assert.equal(result, 'test 110')
  })

  test('calls the String function', ({ assert }) => {
    const result = instance._evalFunctions('test String.toLowerCase(TEST)', ctx)
    assert.equal(result, 'test test')
  })

  test('calls the encodeURIComponent()', ({ assert }) => {
    const result = instance._evalFunctions('test encodeURIComponent(te s+t)', ctx)
    assert.equal(result, 'test te%20s%2Bt')
  })

  test('calls the decodeURIComponent()', ({ assert }) => {
    const result = instance._evalFunctions('test decodeURIComponent(te%20s%2Bt)', ctx)
    assert.equal(result, 'test te s+t')
  })
})

test.group('_callFn()', (group) => {
  let instance: VariablesProcessor
  let ctx: Record<string, string>
  group.each.setup(async () => {
    instance = new VariablesProcessor()
    const vars = VariablesProcessor.createContextFromProperties(variables)
    ctx = await instance.buildContext(vars)
  })

  test('Throws when function do not exists', ({ assert }) => {
    assert.throws(() => {
      instance._callFn(ctx, 'nonExisting')
    })
  })

  test('Throws when namespace function do not exists', ({ assert }) => {
    assert.throws(() => {
      instance._callFn(ctx, 'Something.nonExisting')
    })
  })

  test('Calls the EvalFunctions.Now() function', ({ assert }) => {
    const spy = sinon.spy(EvalFunctions, 'Now')
    instance._callFn(ctx, 'now')
    spy.restore()
    assert.isTrue(spy.called)
  })

  test('Calls the EvalFunctions.Random() function', ({ assert }) => {
    const spy = sinon.spy(EvalFunctions, 'Random')
    instance._callFn(ctx, 'random')
    spy.restore()
    assert.isTrue(spy.called)
  })

  test('Calls the EvalFunctions.EncodeURIComponent() function', ({ assert }) => {
    const spy = sinon.spy(EvalFunctions, 'EncodeURIComponent')
    instance._callFn(ctx, 'encodeURIComponent', ['a'])
    assert.isTrue(spy.called)
    spy.restore()
  })

  test('Calls the EvalFunctions.DecodeURIComponent() function', ({ assert }) => {
    const spy = sinon.spy(EvalFunctions, 'DecodeURIComponent')
    instance._callFn(ctx, 'decodeURIComponent', ['a'])
    assert.isTrue(spy.called)
    spy.restore()
  })

  test('Calls the Math.xxx() function', ({ assert }) => {
    const spy = sinon.spy(instance, '_callNamespaceFunction')
    instance._callFn(ctx, 'Math.abs', ['1'])
    assert.isTrue(spy.called)
    assert.deepEqual(spy.args[0][0], ctx, 'context is set')
    assert.equal(spy.args[0][1], 'Math', 'namespace is set')
    assert.equal(spy.args[0][2], 'abs', 'function name is set')
    assert.deepEqual(spy.args[0][3], ['1'], 'arguments is set')
  })

  test('Calls the String.xxx() function', ({ assert }) => {
    const spy = sinon.spy(instance, '_callNamespaceFunction')
    instance._callFn(ctx, 'String.substr', ['test', '1'])
    assert.isTrue(spy.called)
    assert.deepEqual(spy.args[0][0], ctx, 'context is set')
    assert.equal(spy.args[0][1], 'String', 'namespace is set')
    assert.equal(spy.args[0][2], 'substr', 'function name is set')
    assert.typeOf(spy.args[0][3], 'array', 'arguments is set')
  })
})
