// *****************************************************************************
// Copyright 2022-2023 Aerospike, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// *****************************************************************************

'use strict'

/* eslint-env mocha */
/* global expect */

import { exp as expModule, operations, lists as listsModule, Client as Cli, Key, RemovePolicyOptions, AerospikeExp, AerospikeRecord, cdt, AerospikeBins, AerospikeError} from '../lib/aerospike.js';
import * as Aerospike from '../lib/aerospike.js';

import { expect } from 'chai'; 
import * as helper from './test_helper.ts';

const exp: typeof expModule = Aerospike.exp
const op: typeof operations = Aerospike.operations
const lists: typeof listsModule = Aerospike.lists
const Context: typeof cdt.Context = Aerospike.cdt.Context

const keygen: any = helper.keygen

const tempBin: string = 'ExpVar'
const FILTERED_OUT: number = Aerospike.status.FILTERED_OUT

describe('Aerospike.exp_operations', function () {
  helper.skipUnlessVersion('>= 5.0.0', this)

  const client: Cli = helper.client

  const key: Key = keygen.string(helper.namespace, helper.set, { prefix: 'test/exp' })()

  async function createRecord (bins: any, meta = null) {
    await client.put(key, bins, meta)
    return key
  }

  async function testNoMatch (key: any, filterExpression: any) {
    const rejectPolicy: RemovePolicyOptions = { filterExpression }
    let operationSuccessful: boolean = false
    try {
      await client.remove(key, rejectPolicy)
      operationSuccessful = true
    } catch (error: any) {
      expect(error.code).to.eq(FILTERED_OUT, `Received unexpected error code with message "${error.message}"`)
    }
    if (operationSuccessful) {
      expect.fail('Test no-match: Operation should have not have been executed due to failed expression match')
    }
  }

  async function testMatch (key: any, filterExpression: any) {
    const passPolicy: RemovePolicyOptions = { filterExpression }
    await client.remove(key, passPolicy)
  }

  it('builds up a filter expression value', function () {
    const filter: AerospikeExp = exp.eq(exp.binInt('intVal'), exp.int(42))
    expect(filter).to.be.an('array')
  })
  describe('list expressions', function () {
    describe('list size', function () {
      it('matches the size of a list value', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        await testNoMatch(key, exp.eq(exp.lists.size(exp.binList('tags')), exp.int(5)))
        await testMatch(key, exp.eq(exp.lists.size(exp.binList('tags')), exp.int(3)))
      })
    })

    describe('list size with context', function () {
      it('matches the size of a list value within a nested context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        await testNoMatch(key, exp.eq(exp.lists.size(exp.binList('tags'), context), exp.int(5)))
        await testMatch(key, exp.eq(exp.lists.size(exp.binList('tags'), context), exp.int(4)))
      })
    })

    describe('inList', function () {
      helper.skipUnlessVersion('>= 8.1.2', this)

      beforeEach(async () => {
        await createRecord({ color: 'blue', qty: 5, rgb: [1, 5, 255] })
      })

      afterEach(async () => {
        // When running the inList tests in CI/CD, sometimes the after hook will trigger a record not found error
        // This is a workaround to prevent the test from failing
        try {
          await client.remove(key)
        }
        catch (error) {
          const ignore_error = error instanceof AerospikeError && error.code == Aerospike.status.ERR_RECORD_NOT_FOUND
          if (ignore_error == false) {
            throw error;
          }
        }
      })

      it('is true when bin string is contained in a literal list (exp.inList case 1)', async function () {
        const ops: operations.Operation[] = [
          exp.operations.read(tempBin,
            exp.inList(exp.binStr('color'), exp.list(['red', 'blue', 'green'])),
            0)
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})
        expect(result.bins![tempBin]).to.equal(true)
      })

      it('is false when a string literal is not in the list (exp.inList case 2)', async function () {
        const ops: operations.Operation[] = [
          exp.operations.read(tempBin,
            exp.inList(exp.str('yellow'), exp.list(['red', 'blue', 'green'])),
            0)
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})
        expect(result.bins![tempBin]).to.equal(false)
      })

      it('is true when bin int is contained in a literal list (exp.inList case 3)', async function () {
        const ops: operations.Operation[] = [
          exp.operations.read(tempBin,
            exp.inList(exp.binInt('qty'), exp.list([1, 5, 10])),
            0)
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})
        expect(result.bins![tempBin]).to.equal(true)
      })

      it('is true when bin int is contained in a integer bin list (exp.inList case 4)', async function () {
        const ops: operations.Operation[] = [
          exp.operations.read(tempBin,
            exp.inList(exp.binInt('qty'), exp.binList('rgb')),
            0)
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})
        expect(result.bins![tempBin]).to.equal(true)
      })
    })

    describe('clear', function () {
      it('removes all items in a map', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.clear(
              exp.binList('tags')),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: [] })
      })

      it('selects item identified by index inside nested map', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })

        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.clear(
              exp.binList('tags'),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', []] })
      })
    })

    describe('removeByValue', function () {
      it('removes list item by value', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByValue(
              exp.binList('tags'),
              exp.str('green')),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'yellow'] })
      })

      it('removes list item by value in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByValue(
              exp.binList('tags'),
              exp.str('white'),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['orange', 'pink', 'black']] })
      })
    })

    describe('removeByValueList', function () {
      it('removes list item by value list', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByValueList(
              exp.binList('tags'),
              exp.list(['green', 'yellow'])),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue'] })
      })

      it('removes list item by value list in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByValueList(
              exp.binList('tags'),
              exp.list(['orange', 'white']),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['pink', 'black']] })
      })
    })

    describe('removeByValueRange', function () {
      it('removes list item by value range', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByValueRange(
              exp.binList('tags'),
              exp.str('green'),
              exp.str('blue')),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['green', 'yellow'] })
      })

      it('removes list item by value range in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByValueRange(
              exp.binList('tags'),
              exp.str('pink'),
              exp.str('black'),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['pink', 'white']] })
      })
    })

    describe('removeByRelRankRangeToEnd', function () {
      it('removes list item by value relative rank range to end', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRelRankRangeToEnd(
              exp.binList('tags'),
              exp.int(1),
              exp.str('blue')),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue'] })
      })

      it('removes list item by value relative rank range to end in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRelRankRangeToEnd(
              exp.binList('tags'),
              exp.int(1),
              exp.str('orange'),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['orange', 'black']] })
      })
    })

    describe('removeByRelRankRange', function () {
      it('removes list item by value relative rank range', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRelRankRange(
              exp.binList('tags'),
              exp.int(1),
              exp.int(-1),
              exp.str('green')),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['green', 'yellow'] })
      })

      it('removes list item by value relative rank range in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRelRankRange(
              exp.binList('tags'),
              exp.int(1),
              exp.int(-1),
              exp.str('pink'),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['pink', 'white', 'black']] })
      })
    })

    describe('removeByIndex', function () {
      it('removes a list item by index', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByIndex(
              exp.binList('tags'),
              exp.int(1)),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})
        expect(result.bins).to.eql({ tags: ['blue', 'yellow'] })
      })

      it('removes a list item by index in a cdt context in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByIndex(
              exp.binList('tags'),
              exp.int(1),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})
        expect(result.bins).to.eql({ tags: ['blue', 'green', ['orange', 'white', 'black']] })
      })
    })

    describe('removeByIndexRangeToEnd', function () {
      it('removes a list item by index range to end', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByIndexRangeToEnd(
              exp.binList('tags'),
              exp.int(1)),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue'] })
      })

      it('removes a list item by index range to end in a cdt context in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByIndexRangeToEnd(
              exp.binList('tags'),
              exp.int(1),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['orange']] })
      })
    })

    describe('removeByIndexRange', function () {
      it('removes a list item by index range', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByIndexRange(
              exp.binList('tags'),
              exp.int(2),
              exp.int(0)),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['yellow'] })
      })

      it('removes a list item by index range in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByIndexRange(
              exp.binList('tags'),
              exp.int(2),
              exp.int(0),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['white', 'black']] })
      })
    })

    describe('removeByRank', function () {
      it('removes a list item by rank', async function () {
        const key: Key = await createRecord({ tags: ['yellow', 'green', 'blue'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRank(
              exp.binList('tags'),
              exp.int(2)),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['green', 'blue'] })
      })

      it('removes a list item by rank in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRank(
              exp.binList('tags'),
              exp.int(2),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['orange', 'white', 'black']] })
      })
    })

    describe('removeByRankRangeToEnd', function () {
      it('removes a list item by rank range to end', async function () {
        const key: Key = await createRecord({ tags: ['yellow', 'green', 'blue'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRankRangeToEnd(
              exp.binList('tags'),
              exp.int(1)),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue'] })
      })

      it('removes a list item by rank range to end in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRankRangeToEnd(
              exp.binList('tags'),
              exp.int(1),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['black']] })
      })
    })

    describe('removeByRankRange', function () {
      it('removes a list item by rank range', async function () {
        const key: Key = await createRecord({ tags: ['yellow', 'green', 'blue'] })

        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRankRange(
              exp.binList('tags'),
              exp.int(2),
              exp.int(0)),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['yellow'] })
      })

      it('removes a list item by rank range in a cdt context', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', ['orange', 'pink', 'white', 'black']] })
        const context: cdt.Context = new Context().addListIndex(2)
        const ops: operations.Operation[] = [
          exp.operations.write('tags',
            exp.lists.removeByRankRange(
              exp.binList('tags'),
              exp.int(2),
              exp.int(0),
              context),
            0),
          op.read('tags')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        expect(result.bins).to.eql({ tags: ['blue', 'green', ['pink', 'white']] })
      })
    })

    describe('getByValue', function () {
      it('matches the count of the matched list values', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow', 'green'] })

        await testNoMatch(key, exp.eq(exp.lists.getByValue(exp.binList('tags'), exp.str('green'), lists.returnType.COUNT), exp.int(1)))
        await testMatch(key, exp.eq(exp.lists.getByValue(exp.binList('tags'), exp.str('green'), lists.returnType.COUNT), exp.int(2)))
      })
    })

    describe('getByValue with context', function () {
      it('matches the count of the matched list values', async function () {
        const key: Key = await createRecord({ tags: ['blue', 'green', 'yellow', 'green', ['orange', 'pink', 'white', 'black', 'pink']] })
        const context: cdt.Context = new Context().addListIndex(4)
        await testNoMatch(key, exp.eq(exp.lists.getByValue(exp.binList('tags'), exp.str('pink'), lists.returnType.COUNT, context), exp.int(1)))
        await testMatch(key, exp.eq(exp.lists.getByValue(exp.binList('tags'), exp.str('pink'), lists.returnType.COUNT, context), exp.int(2)))
      })
    })

    describe('getByValueRange', function () {
      it('matches the count of the matched range of list values', async function () {
        const key: Key = await createRecord({ values: [53, 16, 94, 38, 25, 88, 48] })

        await testNoMatch(key, exp.eq(exp.lists.getByValueRange(exp.binList('values'), exp.int(25), exp.int(50), lists.returnType.COUNT), exp.int(1)))
        await testMatch(key, exp.eq(exp.lists.getByValueRange(exp.binList('values'), exp.int(25), exp.int(50), lists.returnType.COUNT), exp.int(3)))
      })
    })

    describe('getByValueRange with context', function () {
      it('matches the count of the matched range of list values', async function () {
        const key: Key = await createRecord({ values: [53, 16, 94, 38, 25, 88, 48, [1, 92, 94, 96]] })
        const context: cdt.Context = new Context().addListIndex(7)
        await testNoMatch(key, exp.eq(exp.lists.getByValueRange(exp.binList('values'), exp.int(90), exp.int(99), lists.returnType.COUNT, context), exp.int(1)))
        await testMatch(key, exp.eq(exp.lists.getByValueRange(exp.binList('values'), exp.int(90), exp.int(99), lists.returnType.COUNT, context), exp.int(3)))
      })
    })

    describe('getByValueList', function () {
      it('matches the count of the matched values', async function () {
        const key: Key = await createRecord({ values: [53, 16, 94, 38, 25, 88, 88, 48, 16] })

        await testNoMatch(key, exp.eq(exp.lists.getByValueList(exp.binList('values'), exp.list([88, 94]), lists.returnType.COUNT), exp.int(2)))
        await testMatch(key, exp.eq(exp.lists.getByValueList(exp.binList('values'), exp.list([88, 94]), lists.returnType.COUNT), exp.int(3)))
      })
    })

    describe('getByValueList with context', function () {
      it('matches the count of the matched values', async function () {
        const key: Key = await createRecord({ values: [53, 16, 94, 38, 25, 88, 88, 48, 16, [0, 1, 2, 73, 74, 73, 74]] })
        const context: cdt.Context = new Context().addListIndex(9)
        await testNoMatch(key, exp.eq(exp.lists.getByValueList(exp.binList('values'), exp.list([73, 74]), lists.returnType.COUNT, context), exp.int(2)))
        await testMatch(key, exp.eq(exp.lists.getByValueList(exp.binList('values'), exp.list([73, 74]), lists.returnType.COUNT, context), exp.int(4)))
      })
    })

    describe('getByRelRankRangeToEnd', function () {
      it('selects list items nearest to value and greater by relative rank', async function () {
        const key: Key = await createRecord({ values: [53, 16, 94, 38, 25, 88, 88, 48, 16] })

        await testNoMatch(key, exp.eq(exp.lists.getByRelRankRangeToEnd(exp.binList('values'), exp.int(38), exp.int(1), lists.returnType.VALUE), exp.list([38, 48, 53, 88, 88, 94])))
        await testMatch(key, exp.eq(exp.lists.getByRelRankRangeToEnd(exp.binList('values'), exp.int(38), exp.int(1), lists.returnType.VALUE), exp.list([48, 53, 88, 88, 94])))
      })
    })

    describe('getByRelRankRangeToEnd with context', function () {
      it('selects list items nearest to value and greater by relative rank', async function () {
        const key: Key = await createRecord({ values: [53, 16, [2, 12, 14, 17]] })
        const context: cdt.Context = new Context().addListIndex(2)
        await testNoMatch(key, exp.eq(exp.lists.getByRelRankRangeToEnd(exp.binList('values'), exp.int(12), exp.int(1), lists.returnType.VALUE, context), exp.list([16, 53])))
        await testMatch(key, exp.eq(exp.lists.getByRelRankRangeToEnd(exp.binList('values'), exp.int(12), exp.int(1), lists.returnType.VALUE, context), exp.list([14, 17])))
      })
    })

    describe('getByRelRankRange', function () {
      it('selects list items nearest to value and greater by relative rank with a count limit', async function () {
        const key: Key = await createRecord({ values: [53, 16, 94, 38, 25, 88, 88, 48, 16] })

        await testNoMatch(key, exp.eq(exp.lists.getByRelRankRange(exp.binList('values'), exp.int(38), exp.int(1), exp.int(3), lists.returnType.VALUE), exp.list([38, 48, 53])))
        await testMatch(key, exp.eq(exp.lists.getByRelRankRange(exp.binList('values'), exp.int(38), exp.int(1), exp.int(3), lists.returnType.VALUE), exp.list([48, 53, 88])))
      })
    })

    describe('getByRelRankRange with context', function () {
      it('selects list items nearest to value and greater by relative rank with a count limit', async function () {
        const key: Key = await createRecord({ values: [53, 16, 94, [30, 40, 45, 20]] })
        const context: cdt.Context = new Context().addListIndex(3)
        await testNoMatch(key, exp.eq(exp.lists.getByRelRankRange(exp.binList('values'), exp.int(30), exp.int(1), exp.int(3), lists.returnType.VALUE, context), exp.list([94])))
        await testMatch(key, exp.eq(exp.lists.getByRelRankRange(exp.binList('values'), exp.int(30), exp.int(1), exp.int(3), lists.returnType.VALUE, context), exp.list([40, 45])))
      })
    })

    describe('getByIndex', function () {
      it('selects item identified by index', async function () {
        const key: Key = await createRecord({ values: ['Singapore', 'Hamburg', 'San Francisco', 'Tokyo'] })

        await testNoMatch(key, exp.eq(exp.lists.getByIndex(exp.binList('values'), exp.int(2), exp.type.STR, lists.returnType.VALUE), exp.str('Hamburg')))
        await testMatch(key, exp.eq(exp.lists.getByIndex(exp.binList('values'), exp.int(2), exp.type.STR, lists.returnType.VALUE), exp.str('San Francisco')))
      })
    })

    describe('getByIndex with context', function () {
      it('selects item identified by index within nested context', async function () {
        const key: Key = await createRecord({ values: ['Singapore', 'Hamburg', 'San Francisco', 'Tokyo', ['Firth', 'Hickman', 'Palmyra']] })
        const context: cdt.Context = new Context().addListIndex(4)
        await testNoMatch(key, exp.eq(exp.lists.getByIndex(exp.binList('values'), exp.int(2), exp.type.STR, lists.returnType.VALUE, context), exp.str('San Francisco')))
        await testMatch(key, exp.eq(exp.lists.getByIndex(exp.binList('values'), exp.int(2), exp.type.STR, lists.returnType.VALUE, context), exp.str('Palmyra')))
      })
    })

    describe('getByIndexRangeToEnd', function () {
      it('selects list items starting at specified index to the end of the list', async function () {
        const key: Key = await createRecord({ values: ['Singapore', 'Hamburg', 'San Francisco', 'Tokyo'] })

        await testNoMatch(key, exp.eq(exp.lists.getByIndexRangeToEnd(exp.binList('values'), exp.int(2), lists.returnType.VALUE), exp.list(['Hamburg', 'San Francisco'])))
        await testMatch(key, exp.eq(exp.lists.getByIndexRangeToEnd(exp.binList('values'), exp.int(2), lists.returnType.VALUE), exp.list(['San Francisco', 'Tokyo'])))
      })
    })

    describe('getByIndexRangeToEnd with context', function () {
      it('selects list items starting at specified index to the end of the list', async function () {
        const key: Key = await createRecord({ values: ['Singapore', 'Hamburg', 'San Francisco', 'Tokyo', ['Firth', 'Hickman', 'Palmyra']] })
        const context: cdt.Context = new Context().addListIndex(4)
        await testNoMatch(key, exp.eq(exp.lists.getByIndexRangeToEnd(exp.binList('values'), exp.int(1), lists.returnType.VALUE, context), exp.list(['Hamburg', 'San Francisco', 'Tokyo'])))
        await testMatch(key, exp.eq(exp.lists.getByIndexRangeToEnd(exp.binList('values'), exp.int(1), lists.returnType.VALUE, context), exp.list(['Hickman', 'Palmyra'])))
      })
    })

    describe('getByIndexRange', function () {
      it('selects "count" list items starting at specified index', async function () {
        const key: Key = await createRecord({ values: ['Singapore', 'Hamburg', 'San Francisco', 'Tokyo'] })

        await testNoMatch(key, exp.eq(exp.lists.getByIndexRange(exp.binList('values'), exp.int(2), exp.int(1), lists.returnType.VALUE), exp.list(['Hamburg'])))
        await testMatch(key, exp.eq(exp.lists.getByIndexRange(exp.binList('values'), exp.int(2), exp.int(1), lists.returnType.VALUE), exp.list(['San Francisco'])))
      })
    })

    describe('getByIndexRange with context', function () {
      it('selects "count" list items starting at specified index', async function () {
        const key: Key = await createRecord({ values: ['Singapore', 'Hamburg', 'San Francisco', 'Tokyo', ['Firth', 'Hickman', 'Palmyra']] })
        const context: cdt.Context = new Context().addListIndex(4)
        await testNoMatch(key, exp.eq(exp.lists.getByIndexRange(exp.binList('values'), exp.int(0), exp.int(2), lists.returnType.VALUE, context), exp.list(['Singapore', 'Hamburg'])))
        await testMatch(key, exp.eq(exp.lists.getByIndexRange(exp.binList('values'), exp.int(0), exp.int(2), lists.returnType.VALUE, context), exp.list(['Firth', 'Hickman'])))
      })
    })

    describe('getByRank', function () {
      it('selects list item identified by rank', async function () {
        const key: Key = await createRecord({ values: [83, 39, 49, 20, 42, 41, 98] })

        await testNoMatch(key, exp.eq(exp.lists.getByRank(exp.binList('values'), exp.int(2), exp.type.INT, lists.returnType.VALUE), exp.int(42)))
        await testMatch(key, exp.eq(exp.lists.getByRank(exp.binList('values'), exp.int(2), exp.type.INT, lists.returnType.VALUE), exp.int(41)))
      })
    })

    describe('getByRank with context', function () {
      it('selects list item identified by rank', async function () {
        const key: Key = await createRecord({ values: [83, [0, 4, 2, 8], 40] })
        const context: cdt.Context = new Context().addListIndex(1)
        await testNoMatch(key, exp.eq(exp.lists.getByRank(exp.binList('values'), exp.int(2), exp.type.INT, lists.returnType.VALUE, context), exp.int(40)))
        await testMatch(key, exp.eq(exp.lists.getByRank(exp.binList('values'), exp.int(2), exp.type.INT, lists.returnType.VALUE, context), exp.int(4)))
      })
    })

    describe('getByRankRangeToEnd', function () {
      it('selects list items starting at specified rank to the last ranked item', async function () {
        const key: Key = await createRecord({ values: [83, 39, 49, 20, 42, 41, 98] })

        await testNoMatch(key, exp.eq(exp.lists.getByRankRangeToEnd(exp.binList('values'), exp.int(2), lists.returnType.VALUE), exp.list([39, 41, 42, 49, 83, 98])))
        await testMatch(key, exp.eq(exp.lists.getByRankRangeToEnd(exp.binList('values'), exp.int(2), lists.returnType.VALUE), exp.list([41, 42, 49, 83, 98])))
      })
    })

    describe('getByRankRangeToEnd with context', function () {
      it('selects list items starting at specified rank to the last ranked item', async function () {
        const key: Key = await createRecord({ values: [83, [0, 4, 2, 8]] })
        const context: cdt.Context = new Context().addListIndex(1)
        await testNoMatch(key, exp.eq(exp.lists.getByRankRangeToEnd(exp.binList('values'), exp.int(1), lists.returnType.VALUE, context), exp.list([0, 2, 4, 8])))
        await testMatch(key, exp.eq(exp.lists.getByRankRangeToEnd(exp.binList('values'), exp.int(1), lists.returnType.VALUE, context), exp.list([2, 4, 8])))
      })
    })

    describe('getByRankRange', function () {
      it('selects "count" list items starting at specified rank', async function () {
        const key: Key = await createRecord({ values: [83, 39, 49, 20, 42, 41, 98] })

        await testNoMatch(key, exp.eq(exp.lists.getByRankRange(exp.binList('values'), exp.int(2), exp.int(2), lists.returnType.VALUE), exp.list([39, 41, 42])))
        await testMatch(key, exp.eq(exp.lists.getByRankRange(exp.binList('values'), exp.int(2), exp.int(2), lists.returnType.VALUE), exp.list([42, 41])))
      })
    })

    describe('getByRankRange with context', function () {
      it('selects "count" list items starting at specified rank', async function () {
        const key: Key = await createRecord({ values: [83, [0, 4, 2, 8]] })
        const context: cdt.Context = new Context().addListIndex(1)
        await testNoMatch(key, exp.eq(exp.lists.getByRankRange(exp.binList('values'), exp.int(1), exp.int(4), lists.returnType.VALUE, context), exp.list([83, [0, 4, 2, 8]])))
        await testMatch(key, exp.eq(exp.lists.getByRankRange(exp.binList('values'), exp.int(1), exp.int(4), lists.returnType.VALUE, context), exp.list([2, 4, 8])))
      })
    })

    describe('list bin append expression', function () {
      it('appends integer value to list', async function () {
        const key: Key = await createRecord({ list: [2, 3, 4, 5], intVal: 6 })
        const ops: operations.Operation[] = [
          exp.operations.read(tempBin,
            exp.lists.append(exp.binList('list'), exp.binInt('intVal')),
            0),
          op.read('list')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        const bins: AerospikeBins = result.bins
        expect(bins.list).to.eql([2, 3, 4, 5])
        expect(bins.ExpVar).to.eql([2, 3, 4, 5, 6])
      })

      it('appends integer value to a list within a nested context', async function () {
        const key: Key = await createRecord({ list: [2, 3, 4, 5, [4]], intVal: 6 })
        const context: cdt.Context = new Context().addListIndex(4)
        const ops: operations.Operation[] = [
          exp.operations.read(tempBin,
            exp.lists.append(exp.binList('list'), exp.binInt('intVal'), null, context),
            0),
          op.read('list')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        const bins: AerospikeBins = result.bins
        expect(bins.list).to.eql([2, 3, 4, 5, [4]])
        expect(bins.ExpVar).to.eql([2, 3, 4, 5, [4, 6]])
      })
    })

    describe('list bin appendItems expression', function () {
      it('appends list to itself', async function () {
        const key: Key = await createRecord({ list: [2, 3, 4, 5] })
        const ops: operations.Operation[] = [
          exp.operations.read(tempBin,
            exp.lists.appendItems(exp.binList('list'), exp.binList('list')),
            0),
          op.read('list')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        const bins: AerospikeBins = result.bins
        expect(bins.list).to.eql([2, 3, 4, 5])
        expect(bins.ExpVar).to.eql([2, 3, 4, 5, 2, 3, 4, 5])
      })

      it('appends list to a list within a nested context', async function () {
        const key: Key = await createRecord({ list: [2, 3, 4, 5, [80, 90, 100]] })
        const context: cdt.Context = new Context().addListIndex(4)
        const ops: operations.Operation[] = [
          exp.operations.read(tempBin,
            exp.lists.appendItems(exp.binList('list'), exp.binList('list'), null, context),
            0),
          op.read('list')
        ]
        const result: AerospikeRecord = await client.operate(key, ops, {})

        const bins: AerospikeBins = result.bins
        expect(bins.list).to.eql([2, 3, 4, 5, [80, 90, 100]])
        expect(bins.ExpVar).to.eql([2, 3, 4, 5, [80, 90, 100, 2, 3, 4, 5, [80, 90, 100]]])
      })
    })
  })
  describe('list bin insert expression', function () {
    it('inserts value at specified index', async function () {
      const key: Key = await createRecord({ list: [2, 3, 4, 5], intVal: 6 })
      const ops: operations.Operation[] = [
        exp.operations.read(tempBin,
          exp.lists.insert(exp.binList('list'), exp.binInt('intVal'), exp.int(2)),
          0),
        op.read('list')
      ]
      const result: AerospikeRecord = await client.operate(key, ops, {})

      const bins: AerospikeBins = result.bins
      expect(bins.list).to.eql([2, 3, 4, 5])
      expect(bins.ExpVar).to.eql([2, 3, 6, 4, 5])
    })

    it('inserts value at specified index within a nested context', async function () {
      const key: Key = await createRecord({ list: [2, 3, 4, 5, [4, 1, 9]], intVal: 7 })
      const context: cdt.Context = new Context().addListIndex(4)
      const ops: operations.Operation[] = [
        exp.operations.read(tempBin,
          exp.lists.insert(exp.binList('list'), exp.binInt('intVal'), exp.int(2), null, context),
          0),
        op.read('list')
      ]
      const result: AerospikeRecord = await client.operate(key, ops, {})

      const bins: AerospikeBins = result.bins
      expect(bins.list).to.eql([2, 3, 4, 5, [4, 1, 9]])
      expect(bins.ExpVar).to.eql([2, 3, 4, 5, [4, 1, 7, 9]])
    })
  })
  describe('list bin insertItems expression', function () {
    it('inserts values at specified index', async function () {
      const key: Key = await createRecord({ list: [2, 3, 4, 5] })
      const ops: operations.Operation[] = [
        exp.operations.read(tempBin,
          exp.lists.insertItems(exp.binList('list'), exp.binList('list'), exp.int(1)),
          0),
        op.read('list')
      ]
      const result: AerospikeRecord = await client.operate(key, ops, {})

      const bins: AerospikeBins = result.bins
      expect(bins.list).to.eql([2, 3, 4, 5])
      expect(bins.ExpVar).to.eql([2, 2, 3, 4, 5, 3, 4, 5])
    })

    it('inserts values at specified index within a nested context', async function () {
      const key: Key = await createRecord({ list: [2, 3, [9, 9]] })
      const context: cdt.Context = new Context().addListIndex(2)
      const ops: operations.Operation[] = [
        exp.operations.read(tempBin,
          exp.lists.insertItems(exp.binList('list'), exp.binList('list'), exp.int(1), null, context),
          0),
        op.read('list')
      ]
      const result: AerospikeRecord = await client.operate(key, ops, {})

      const bins: AerospikeBins = result.bins
      expect(bins.list).to.eql([2, 3, [9, 9]])
      expect(bins.ExpVar).to.eql([2, 3, [9, 2, 3, [9, 9], 9]])
    })
  })

  describe('list bin sort expression', function () {
    it('sorts specified list', async function () {
      const key: Key = await createRecord({ list: [2, 3, 4, 5] })
      const ops: operations.Operation[] = [
        exp.operations.write('list',
          exp.lists.insertItems(exp.binList('list'), exp.binList('list'), exp.int(1)),
          0),
        exp.operations.read(tempBin,
          exp.lists.sort(exp.binList('list'), 1),
          0),
        op.read('list')
      ]
      const result: AerospikeRecord = await client.operate(key, ops, {})

      const bins: AerospikeBins = result.bins
      expect(bins.ExpVar).to.eql([5, 5, 4, 4, 3, 3, 2, 2])
      expect(bins.list).to.eql([2, 2, 3, 4, 5, 3, 4, 5])
    })

    it('sorts specified nested list', async function () {
      const key: Key = await createRecord({ list: [2, 3, 4, 5, [9, 100]] })
      const context: cdt.Context = new Context().addListIndex(4)
      const ops: operations.Operation[] = [
        exp.operations.read(tempBin,
          exp.lists.sort(exp.binList('list'), 1, context),
          0),
        op.read('list')
      ]
      const result: AerospikeRecord = await client.operate(key, ops, {})

      const bins: AerospikeBins = result.bins
      expect(bins.ExpVar).to.eql([2, 3, 4, 5, [100, 9]])
      expect(bins.list).to.eql([2, 3, 4, 5, [9, 100]])
    })
  })
})
